From ccf0621b382b0f233df0d7a08e99f1d947fb0d67 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 2 Dec 2019 21:47:02 -0500 Subject: [PATCH 01/86] WIP setting up react, redux, etc --- .../embeddables/resolver/embeddable.tsx | 26 +++++++++++++++++-- .../public/embeddables/resolver/factory.ts | 11 ++++++-- x-pack/plugins/endpoint/public/plugin.ts | 3 ++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index 55f9fd52f46627..10d8bdebdc5752 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpServiceBase } from 'kibana/public'; +import ReactDOM from 'react-dom'; +import React from 'react'; import { EmbeddableInput, IContainer, @@ -12,7 +15,13 @@ import { export class ResolverEmbeddable extends Embeddable { public readonly type = 'resolver'; - constructor(initialInput: EmbeddableInput, parent?: IContainer) { + private httpServiceBase: HttpServiceBase; + private lastRenderTarget?: Element; + constructor( + initialInput: EmbeddableInput, + httpServiceBase: HttpServiceBase, + parent?: IContainer + ) { super( // Input state is irrelevant to this embeddable, just pass it along. initialInput, @@ -22,13 +31,26 @@ export class ResolverEmbeddable extends Embeddable { // Optional parent component, this embeddable can optionally be rendered inside a container. parent ); + this.httpServiceBase = httpServiceBase; } public render(node: HTMLElement) { - node.innerHTML = '
Welcome from Resolver
'; + if (this.lastRenderTarget !== undefined) { + ReactDOM.unmountComponentAtNode(this.lastRenderTarget); + } + this.lastRenderTarget = node; + // TODO, figure out how to destroy middleware + const store = storeFactory(); + ReactDOM.render(, node); } public reload(): void { throw new Error('Method not implemented.'); } + + public destroy(): void { + if (this.lastRenderTarget !== undefined) { + ReactDOM.unmountComponentAtNode(this.lastRenderTarget); + } + } } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts index aef2e309254ef0..63a0a2021758a4 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -5,22 +5,29 @@ */ import { i18n } from '@kbn/i18n'; -import { ResolverEmbeddable } from './'; +import { HttpServiceBase } from 'kibana/public'; import { EmbeddableFactory, EmbeddableInput, IContainer, } from '../../../../../../src/plugins/embeddable/public'; +import { ResolverEmbeddable } from './'; export class ResolverEmbeddableFactory extends EmbeddableFactory { public readonly type = 'resolver'; + private httpServiceBase: HttpServiceBase; + + constructor(httpServiceBase: HttpServiceBase) { + super(); + this.httpServiceBase = httpServiceBase; + } public isEditable() { return true; } public async create(initialInput: EmbeddableInput, parent?: IContainer) { - return new ResolverEmbeddable(initialInput, parent); + return new ResolverEmbeddable(initialInput, this.httpServiceBase, parent); } public getDisplayName() { diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 02514cc974af05..219477698c2642 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -26,7 +26,6 @@ export class EndpointPlugin EndpointPluginStartDependencies > { public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) { - const resolverEmbeddableFactory = new ResolverEmbeddableFactory(); core.application.register({ id: 'endpoint', title: i18n.translate('xpack.endpoint.pluginTitle', { @@ -39,6 +38,8 @@ export class EndpointPlugin }, }); + const resolverEmbeddableFactory = new ResolverEmbeddableFactory(core.http); + plugins.embeddable.registerEmbeddableFactory( resolverEmbeddableFactory.type, resolverEmbeddableFactory From c88400c8a057eed383f5681114593c4ac61e0cb2 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 09:10:00 -0500 Subject: [PATCH 02/86] WIP more --- .../public/embeddables/resolver/actions.ts | 24 +++++++++++++++++++ .../embeddables/resolver/embeddable.tsx | 4 ++-- .../embeddables/resolver/store/index.ts | 14 +++++++++++ .../embeddables/resolver/store/reducer.ts | 13 ++++++++++ .../public/embeddables/resolver/types.ts | 8 +++++++ 5 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/types.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts new file mode 100644 index 00000000000000..7554435467b1c9 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function userPanned(offset: [number, number]) { + return { + type: 'userPanned' as const, + payload: offset, + }; +} + +export function userZoomed(zoomLevel: number) { + return { + type: 'userZoomed' as const, + payload: zoomLevel, + }; +} + +type userPannedAction = ReturnType; +type userZoomedAction = ReturnType; + +export type ResolverAction = userPannedAction | userZoomedAction; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index 10d8bdebdc5752..e7de47b14766c7 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -40,8 +40,8 @@ export class ResolverEmbeddable extends Embeddable { } this.lastRenderTarget = node; // TODO, figure out how to destroy middleware - const store = storeFactory(); - ReactDOM.render(, node); + const { store } = storeFactory(); + ReactDOM.render(, node); } public reload(): void { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts new file mode 100644 index 00000000000000..47aaa1d1b993fe --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createStore } from 'redux'; + +export const storeFactory = () => { + const store = createStore(reducer, undefined); + return { + store, + }; +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts new file mode 100644 index 00000000000000..a923def4784281 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ResolverState, ResolverAction } from '../types'; +export function reducer(state: ResolverState = true, action: ResolverAction) { + if (action.type === 'shut') { + return state; + } else { + return !state; + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts new file mode 100644 index 00000000000000..db60ae7e520af0 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export type ResolverState = boolean; + +export { ResolverAction } from './actions'; From a07d0fd98224853e777668d8b021da9cb9e32374 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 10:51:00 -0500 Subject: [PATCH 03/86] WIP even more --- .../public/embeddables/resolver/actions.ts | 19 +-------- .../resolver/store/camera/action.ts | 22 +++++++++++ .../resolver/store/camera/index.ts | 8 ++++ .../resolver/store/camera/reducer.ts | 39 +++++++++++++++++++ .../resolver/store/camera/selectors.ts | 21 ++++++++++ .../embeddables/resolver/store/index.ts | 3 +- .../embeddables/resolver/store/reducer.ts | 13 +++---- .../public/embeddables/resolver/types.ts | 12 +++++- 8 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts index 7554435467b1c9..ec0e875bbef0fc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts @@ -3,22 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { CameraAction } from './store/camera'; -export function userPanned(offset: [number, number]) { - return { - type: 'userPanned' as const, - payload: offset, - }; -} +export type ResolverAction = CameraAction; -export function userZoomed(zoomLevel: number) { - return { - type: 'userZoomed' as const, - payload: zoomLevel, - }; -} - -type userPannedAction = ReturnType; -type userZoomedAction = ReturnType; - -export type ResolverAction = userPannedAction | userZoomedAction; 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 new file mode 100644 index 00000000000000..1161c3cd360465 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface UserZoomed { + readonly type: 'userZoomed'; + readonly payload: number; +} + +export interface UserPanned { + readonly type: 'userPanned'; + readonly payload: readonly [number, number]; +} + +export interface UserSetRasterSize { + readonly type: 'userSetRasterSize'; + readonly payload: readonly [number, number]; +} + +export type CameraAction = UserZoomed | UserPanned | UserSetRasterSize; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts new file mode 100644 index 00000000000000..a12054496583dc --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { cameraReducer } from './reducer'; +export { CameraAction } from './action'; 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 new file mode 100644 index 00000000000000..0f5e9a73bc6a5b --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Reducer } from 'redux'; +import { CameraState, ResolverAction } from '../../types'; + +function initialState() { + return { + zoomLevel: 1, + panningOffset: [0, 0] as const, + rasterSize: [0, 0] as const, + }; +} + +export const cameraReducer: Reducer = ( + state = initialState(), + action +) => { + if (action.type === 'userZoomed') { + return { + ...state, + scaling: action.payload, + }; + } else if (action.type === 'userPanned') { + return { + ...state, + panningOffset: action.payload, + }; + } else if (action.type === 'userSetRasterSize') { + return { + ...state, + rasterSize: action.payload, + }; + } else { + return state; + } +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts new file mode 100644 index 00000000000000..47188948ba06e7 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Vector2, CameraState } from '../../types'; + +/** + * https://en.wikipedia.org/wiki/Orthographic_projection + */ +export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { + const right = (state.rasterSize[0] / 2) * state.zoomLevel + state.panningOffset[0]; + const left = (state.rasterSize[0] / -2) * state.zoomLevel + state.panningOffset[0]; + const top = (state.rasterSize[1] / 2) * state.zoomLevel + state.panningOffset[1]; + const bottom = (state.rasterSize[1] / -2) * state.zoomLevel + state.panningOffset[1]; + return ([worldX, worldY]) => [ + worldX * (state.rasterSize[0] / (right - left)) - (right + left) / (right - left), + worldY * (state.rasterSize[1] / (top - bottom)) - (top + bottom) / (top - bottom), + ]; +}; 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 47aaa1d1b993fe..298112418735a0 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -5,9 +5,10 @@ */ import { createStore } from 'redux'; +import { resolverReducer } from './reducer'; export const storeFactory = () => { - const store = createStore(reducer, undefined); + const store = createStore(resolverReducer, undefined); return { store, }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts index a923def4784281..5f7d140a0bed57 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts @@ -3,11 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Reducer, combineReducers } from 'redux'; +import { cameraReducer } from './camera/reducer'; import { ResolverState, ResolverAction } from '../types'; -export function reducer(state: ResolverState = true, action: ResolverAction) { - if (action.type === 'shut') { - return state; - } else { - return !state; - } -} + +export const resolverReducer: Reducer = combineReducers({ + camera: cameraReducer, +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index db60ae7e520af0..0a1efb37bf1676 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -3,6 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export type ResolverState = boolean; +export interface ResolverState { + camera: CameraState; +} export { ResolverAction } from './actions'; + +export interface CameraState { + readonly zoomLevel: number; + readonly panningOffset: readonly [number, number]; + readonly rasterSize: readonly [number, number]; +} + +export type Vector2 = readonly [number, number]; From b0203ae8eb8833607200b90e5ee3215b3706f462 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 11:16:57 -0500 Subject: [PATCH 04/86] wip more. about to make a component and test worldToRaster --- .../embeddables/resolver/embeddable.tsx | 6 +++-- .../resolver/store/camera/action.ts | 6 +++-- .../embeddables/resolver/store/index.ts | 5 ++-- .../embeddables/resolver/store/selectors.ts | 24 +++++++++++++++++ .../public/embeddables/resolver/types.ts | 6 +++-- .../embeddables/resolver/view/index.tsx | 26 +++++++++++++++++++ 6 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index e7de47b14766c7..0e7881223c0947 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -7,6 +7,8 @@ import { HttpServiceBase } from 'kibana/public'; import ReactDOM from 'react-dom'; import React from 'react'; +import { AppRoot } from './view'; +import { storeFactory } from './store'; import { EmbeddableInput, IContainer, @@ -40,8 +42,8 @@ export class ResolverEmbeddable extends Embeddable { } this.lastRenderTarget = node; // TODO, figure out how to destroy middleware - const { store } = storeFactory(); - ReactDOM.render(, node); + const { store } = storeFactory({ httpServiceBase }); + ReactDOM.render(, node); } public reload(): void { 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 1161c3cd360465..d94d6b34c36cb7 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 @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Vector2 } from '../../types'; + export interface UserZoomed { readonly type: 'userZoomed'; readonly payload: number; @@ -11,12 +13,12 @@ export interface UserZoomed { export interface UserPanned { readonly type: 'userPanned'; - readonly payload: readonly [number, number]; + readonly payload: Vector2; } export interface UserSetRasterSize { readonly type: 'userSetRasterSize'; - readonly payload: readonly [number, number]; + readonly payload: Vector2; } export type CameraAction = UserZoomed | UserPanned | UserSetRasterSize; 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 298112418735a0..9c759dfcf1c3d7 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 } from 'redux'; +import { HttpServiceBase } from 'kibana/public'; import { resolverReducer } from './reducer'; -export const storeFactory = () => { - const store = createStore(resolverReducer, undefined); +export const storeFactory = ({ httpServiceBase }: { httpServiceBase: HttpServiceBase }) => { + const store = createStore(resolverReducer, undefined, applyMiddleware()); return { store, }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts new file mode 100644 index 00000000000000..15c8634e94c042 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as cameraSelectors from './camera/selectors'; +import { ResolverState, ResolverSelector } from '../types'; + +export const worldToRaster: ResolverSelector = composeSelectors( + cameraStateSelector, + cameraSelectors.worldToRaster +); + +function cameraStateSelector(state: ResolverState) { + return state.camera; +} + +function composeSelectors( + selector: (state: OuterState) => InnerState, + secondSelector: (state: InnerState) => ReturnValue +): (state: OuterState) => ReturnValue { + return state => secondSelector(selector(state)); +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 0a1efb37bf1676..1c32dc5623d9ea 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -11,8 +11,10 @@ export { ResolverAction } from './actions'; export interface CameraState { readonly zoomLevel: number; - readonly panningOffset: readonly [number, number]; - readonly rasterSize: readonly [number, number]; + readonly panningOffset: Vector2; + readonly rasterSize: Vector2; } export type Vector2 = readonly [number, number]; + +export type ResolverSelector = (state: ResolverState) => unknown; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx new file mode 100644 index 00000000000000..932261094a6035 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Store } from 'redux'; +import { Provider, useSelector } from 'react-redux'; +import { ResolverState, ResolverAction } from '../types'; +import * as selectors from '../store/selectors'; + +export const AppRoot: React.FC<{ + store: Store; +}> = React.memo(({ store }) => { + return ( + + + + ); +}); + +const Diagnostic: React.FC<{}> = React.memo(() => { + const worldToRaster = useSelector(selectors.worldToRaster); + return
frig
; +}); From 3e530e6c864e38a4eb6308dd187cf8e826bc8be0 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 14:06:23 -0500 Subject: [PATCH 05/86] added react-redux types. working on world to raster --- package.json | 1 + x-pack/plugins/endpoint/package.json | 14 +++++ .../resolver/store/camera/selectors.ts | 50 ++++++++++++++++-- .../store/camera/world_to_raster.test.ts | 52 +++++++++++++++++++ .../embeddables/resolver/store/selectors.ts | 7 +-- .../public/embeddables/resolver/types.ts | 2 - yarn.lock | 10 ++++ 7 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/endpoint/package.json create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts diff --git a/package.json b/package.json index 546a19c6eaba50..20738ab725a6c3 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "x-pack", "x-pack/plugins/*", "x-pack/legacy/plugins/*", + "x-pack/plugins/endpoint", "examples/*", "test/plugin_functional/plugins/*", "test/interpreter_functional/plugins/*" diff --git a/x-pack/plugins/endpoint/package.json b/x-pack/plugins/endpoint/package.json new file mode 100644 index 00000000000000..8efd0eab0eee0d --- /dev/null +++ b/x-pack/plugins/endpoint/package.json @@ -0,0 +1,14 @@ +{ + "author": "Elastic", + "name": "endpoint", + "version": "0.0.0", + "private": true, + "license": "Elastic-License", + "scripts": {}, + "dependencies": { + "react-redux": "^7.1.0" + }, + "devDependencies": { + "@types/react-redux": "^7.1.0" + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 47188948ba06e7..f5fbf4e70f781e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -8,14 +8,56 @@ import { Vector2, CameraState } from '../../types'; /** * https://en.wikipedia.org/wiki/Orthographic_projection + * */ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { - const right = (state.rasterSize[0] / 2) * state.zoomLevel + state.panningOffset[0]; - const left = (state.rasterSize[0] / -2) * state.zoomLevel + state.panningOffset[0]; - const top = (state.rasterSize[1] / 2) * state.zoomLevel + state.panningOffset[1]; - const bottom = (state.rasterSize[1] / -2) * state.zoomLevel + state.panningOffset[1]; + console.log('raster size', state.rasterSize); + console.log('panning offset', state.panningOffset); + console.log('zoom level', state.zoomLevel); + const viewWidth = state.rasterSize[0]; + const viewHeight = state.rasterSize[1]; + const halfViewWidth = viewWidth / 2; + const negativeHalfViewWidth = viewWidth / -2; + const halfViewHeight = viewHeight / 2; + const negativeHalfViewHeight = viewHeight / -2; + + const right = halfViewWidth / state.zoomLevel + state.panningOffset[0]; + const left = negativeHalfViewWidth / state.zoomLevel + state.panningOffset[0]; + const top = halfViewHeight / state.zoomLevel + state.panningOffset[1]; + const bottom = negativeHalfViewHeight / state.zoomLevel + state.panningOffset[1]; + + console.log('top', top, 'right', right, 'bottom', bottom, 'left', left); + console.log( + 'x translation', + -(left + right) / viewWidth, + 'y translation', + -(top + bottom) / viewHeight + ); + + /* + raster size [ 300, 200 ] + panning offset [ 0, 0 ] + zoom level 1 + top 100 right 150 bottom -100 left -150 + x translation 0 y translation 0 + */ + + const leftAlign = -left; + const topAlign = (top - bottom) / 2; + return ([worldX, worldY]) => [ + worldX * (viewWidth / (right - left)) + leftAlign, + -worldY * (viewHeight / (top - bottom)) + topAlign, + + /* + // should be centered + worldX * (viewWidth / (right - left)) - (left + right) / viewWidth, + worldY * (viewHeight / (top - bottom)) - (top + bottom) / viewHeight, + */ + /* worldX * (state.rasterSize[0] / (right - left)) - (right + left) / (right - left), worldY * (state.rasterSize[1] / (top - bottom)) - (top + bottom) / (top - bottom), + */ ]; }; + diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts new file mode 100644 index 00000000000000..9175a325fe9579 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Store, createStore } from 'redux'; +import { CameraAction, UserSetRasterSize } from './action'; +import { CameraState } from '../../types'; +import { cameraReducer } from './reducer'; +import { worldToRaster } from './selectors'; + +describe('worldToRaster', () => { + let store: Store; + beforeEach(() => { + store = createStore(cameraReducer, undefined); + }); + describe('when the raster size is 300 x 200 pixels', () => { + beforeEach(() => { + const action: UserSetRasterSize = { type: 'userSetRasterSize', payload: [300, 200] }; + store.dispatch(action); + }); + it('should convert 0,0 (center) in world space to 150,100 in raster space', () => { + expect(worldToRaster(store.getState())([0, 0])).toEqual([150, 100]); + }); + // top + it('should convert 0,100 (top) in world space to 150,0 in raster space', () => { + expect(worldToRaster(store.getState())([0, 100])).toEqual([150, 0]); + }); + it('should convert 150,100 (top right) in world space to 300,0 in raster space', () => { + expect(worldToRaster(store.getState())([150, 100])).toEqual([300, 0]); + }); + it('should convert 150,0 (right) in world space to 300,100 in raster space', () => { + expect(worldToRaster(store.getState())([150, 0])).toEqual([300, 100]); + }); + it('should convert 150,-100 (right bottom) in world space to 300,200 in raster space', () => { + expect(worldToRaster(store.getState())([150, -100])).toEqual([300, 200]); + }); + it('should convert 0,-100 (bottom) in world space to 150,200 in raster space', () => { + expect(worldToRaster(store.getState())([0, -100])).toEqual([150, 200]); + }); + it('should convert -150,-100 (bottom left) in world space to 0,200 in raster space', () => { + expect(worldToRaster(store.getState())([-150, -100])).toEqual([0, 200]); + }); + it('should convert -150,0 (left) in world space to 0,100 in raster space', () => { + expect(worldToRaster(store.getState())([-150, 0])).toEqual([0, 100]); + }); + it('should convert -150,100 (top left) in world space to 0,100 in raster space', () => { + expect(worldToRaster(store.getState())([-150, 100])).toEqual([0, 0]); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 15c8634e94c042..b57a502837a6a8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -5,12 +5,9 @@ */ import * as cameraSelectors from './camera/selectors'; -import { ResolverState, ResolverSelector } from '../types'; +import { ResolverState } from '../types'; -export const worldToRaster: ResolverSelector = composeSelectors( - cameraStateSelector, - cameraSelectors.worldToRaster -); +export const worldToRaster = composeSelectors(cameraStateSelector, cameraSelectors.worldToRaster); function cameraStateSelector(state: ResolverState) { return state.camera; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 1c32dc5623d9ea..a82122594955bf 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -16,5 +16,3 @@ export interface CameraState { } export type Vector2 = readonly [number, number]; - -export type ResolverSelector = (state: ResolverState) => unknown; diff --git a/yarn.lock b/yarn.lock index dfce24ad77d2e2..f8ae804f52e547 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4069,6 +4069,16 @@ "@types/react" "*" redux "^4.0.0" +"@types/react-redux@^7.1.0": + version "7.1.5" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.5.tgz#c7a528d538969250347aa53c52241051cf886bd3" + integrity sha512-ZoNGQMDxh5ENY7PzU7MVonxDzS1l/EWiy8nUhDqxFqUZn4ovboCyvk4Djf68x6COb7vhGTKjyjxHxtFdAA5sUA== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-resize-detector@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/react-resize-detector/-/react-resize-detector-4.0.1.tgz#cc8f012f5957e4826e69b8d2afd59baadcac556c" From e052cd9915644008f843f32f56d7bfe949d1f8ae Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 14:21:52 -0500 Subject: [PATCH 06/86] passing but so wrong --- .../resolver/store/camera/selectors.ts | 28 ++++++------------- .../store/camera/world_to_raster.test.ts | 11 +++++++- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index f5fbf4e70f781e..db887b7e999d7e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -21,33 +21,21 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => const halfViewHeight = viewHeight / 2; const negativeHalfViewHeight = viewHeight / -2; - const right = halfViewWidth / state.zoomLevel + state.panningOffset[0]; - const left = negativeHalfViewWidth / state.zoomLevel + state.panningOffset[0]; - const top = halfViewHeight / state.zoomLevel + state.panningOffset[1]; - const bottom = negativeHalfViewHeight / state.zoomLevel + state.panningOffset[1]; + const right = halfViewWidth / state.zoomLevel; // + state.panningOffset[0]; + const left = negativeHalfViewWidth / state.zoomLevel; // + state.panningOffset[0]; + const top = halfViewHeight / state.zoomLevel; // + state.panningOffset[1]; + const bottom = negativeHalfViewHeight / state.zoomLevel; // + state.panningOffset[1]; console.log('top', top, 'right', right, 'bottom', bottom, 'left', left); - console.log( - 'x translation', - -(left + right) / viewWidth, - 'y translation', - -(top + bottom) / viewHeight - ); - - /* - raster size [ 300, 200 ] - panning offset [ 0, 0 ] - zoom level 1 - top 100 right 150 bottom -100 left -150 - x translation 0 y translation 0 - */ const leftAlign = -left; const topAlign = (top - bottom) / 2; + console.log('leftAlign', leftAlign, 'top align', topAlign); + return ([worldX, worldY]) => [ - worldX * (viewWidth / (right - left)) + leftAlign, - -worldY * (viewHeight / (top - bottom)) + topAlign, + (worldX + state.panningOffset[0]) * (viewWidth / (right - left)) + leftAlign, + -(worldY + state.panningOffset[1]) * (viewHeight / (top - bottom)) + topAlign, /* // should be centered diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index 9175a325fe9579..a5dabc7954d013 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -5,7 +5,7 @@ */ import { Store, createStore } from 'redux'; -import { CameraAction, UserSetRasterSize } from './action'; +import { CameraAction, UserSetRasterSize, UserPanned } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; import { worldToRaster } from './selectors'; @@ -48,5 +48,14 @@ describe('worldToRaster', () => { it('should convert -150,100 (top left) in world space to 0,100 in raster space', () => { expect(worldToRaster(store.getState())([-150, 100])).toEqual([0, 0]); }); + describe('when the user has panned up and to the right by 50', () => { + beforeEach(() => { + const action: UserPanned = { type: 'userPanned', payload: [-50, -50] }; + store.dispatch(action); + }); + it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { + expect(worldToRaster(store.getState())([0, 0]).toString()).toEqual([100, 150].toString()); + }); + }); }); }); From f2d64169d5a2079bab87a5e33a0241bfd37ab2e0 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 14:51:32 -0500 Subject: [PATCH 07/86] this is just wrong --- .../resolver/store/camera/action.ts | 8 ++++---- .../resolver/store/camera/reducer.ts | 6 +++--- .../resolver/store/camera/selectors.ts | 10 +++++----- .../store/camera/world_to_raster.test.ts | 19 ++++++++++++++++++- .../public/embeddables/resolver/types.ts | 2 +- 5 files changed, 31 insertions(+), 14 deletions(-) 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 d94d6b34c36cb7..4c705201f3ec1b 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 @@ -6,9 +6,9 @@ import { Vector2 } from '../../types'; -export interface UserZoomed { - readonly type: 'userZoomed'; - readonly payload: number; +export interface UserScaled { + readonly type: 'userScaled'; + readonly payload: Vector2; } export interface UserPanned { @@ -21,4 +21,4 @@ export interface UserSetRasterSize { readonly payload: Vector2; } -export type CameraAction = UserZoomed | UserPanned | UserSetRasterSize; +export type CameraAction = UserScaled | UserPanned | UserSetRasterSize; 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 0f5e9a73bc6a5b..70a18f527b433f 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 @@ -6,9 +6,9 @@ import { Reducer } from 'redux'; import { CameraState, ResolverAction } from '../../types'; -function initialState() { +function initialState(): CameraState { return { - zoomLevel: 1, + scaling: [1, 1] as const, panningOffset: [0, 0] as const, rasterSize: [0, 0] as const, }; @@ -18,7 +18,7 @@ export const cameraReducer: Reducer = ( state = initialState(), action ) => { - if (action.type === 'userZoomed') { + if (action.type === 'userScaled') { return { ...state, scaling: action.payload, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index db887b7e999d7e..de641a74f89432 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -13,7 +13,7 @@ import { Vector2, CameraState } from '../../types'; export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { console.log('raster size', state.rasterSize); console.log('panning offset', state.panningOffset); - console.log('zoom level', state.zoomLevel); + console.log('zoom level', state.scaling); const viewWidth = state.rasterSize[0]; const viewHeight = state.rasterSize[1]; const halfViewWidth = viewWidth / 2; @@ -21,10 +21,10 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => const halfViewHeight = viewHeight / 2; const negativeHalfViewHeight = viewHeight / -2; - const right = halfViewWidth / state.zoomLevel; // + state.panningOffset[0]; - const left = negativeHalfViewWidth / state.zoomLevel; // + state.panningOffset[0]; - const top = halfViewHeight / state.zoomLevel; // + state.panningOffset[1]; - const bottom = negativeHalfViewHeight / state.zoomLevel; // + state.panningOffset[1]; + const right = halfViewWidth / state.scaling[0]; // + state.panningOffset[0]; + const left = negativeHalfViewWidth / state.scaling[0]; // + state.panningOffset[0]; + const top = halfViewHeight / state.scaling[1]; // + state.panningOffset[1]; + const bottom = negativeHalfViewHeight / state.scaling[1]; // + state.panningOffset[1]; console.log('top', top, 'right', right, 'bottom', bottom, 'left', left); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index a5dabc7954d013..177ca674f7c0f0 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -5,7 +5,7 @@ */ import { Store, createStore } from 'redux'; -import { CameraAction, UserSetRasterSize, UserPanned } from './action'; +import { CameraAction, UserSetRasterSize, UserPanned, UserScaled } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; import { worldToRaster } from './selectors'; @@ -48,6 +48,15 @@ describe('worldToRaster', () => { it('should convert -150,100 (top left) in world space to 0,100 in raster space', () => { expect(worldToRaster(store.getState())([-150, 100])).toEqual([0, 0]); }); + describe('when the user has zoomed to 0.5', () => { + beforeEach(() => { + const action: UserScaled = { type: 'userScaled', payload: [0.5, 0.5] }; + store.dispatch(action); + }); + it('should convert 0, 0 (center) in world space to 150, 100 (center)', () => { + expect(worldToRaster(store.getState())([0, 0])).toEqual([150, 100]); + }); + }); describe('when the user has panned up and to the right by 50', () => { beforeEach(() => { const action: UserPanned = { type: 'userPanned', payload: [-50, -50] }; @@ -56,6 +65,14 @@ describe('worldToRaster', () => { it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { expect(worldToRaster(store.getState())([0, 0]).toString()).toEqual([100, 150].toString()); }); + it('should convert 50,50 (right and up a bit) in world space to 150,100 (center) in raster space', () => { + expect(worldToRaster(store.getState())([50, 50]).toString()).toEqual([150, 100].toString()); + }); + it('should convert 60,-60 (right and down a bit) in world space to 160,210 (center) in raster space', () => { + expect(worldToRaster(store.getState())([60, -60]).toString()).toEqual( + [160, 210].toString() + ); + }); }); }); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index a82122594955bf..8c275f67fc464f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -10,7 +10,7 @@ export interface ResolverState { export { ResolverAction } from './actions'; export interface CameraState { - readonly zoomLevel: number; + readonly scaling: Vector2; readonly panningOffset: Vector2; readonly rasterSize: Vector2; } From 83bf9e522e87bd679c537a68bf0ad917f0b872a9 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 15:43:30 -0500 Subject: [PATCH 08/86] getting some world to raster action --- .../resolver/store/camera/selectors.ts | 81 ++++++++++--------- .../store/camera/world_to_raster.test.ts | 28 ++++++- 2 files changed, 72 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index de641a74f89432..cb5f0780cae2ed 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -11,41 +11,50 @@ import { Vector2, CameraState } from '../../types'; * */ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { - console.log('raster size', state.rasterSize); - console.log('panning offset', state.panningOffset); - console.log('zoom level', state.scaling); - const viewWidth = state.rasterSize[0]; - const viewHeight = state.rasterSize[1]; - const halfViewWidth = viewWidth / 2; - const negativeHalfViewWidth = viewWidth / -2; - const halfViewHeight = viewHeight / 2; - const negativeHalfViewHeight = viewHeight / -2; - - const right = halfViewWidth / state.scaling[0]; // + state.panningOffset[0]; - const left = negativeHalfViewWidth / state.scaling[0]; // + state.panningOffset[0]; - const top = halfViewHeight / state.scaling[1]; // + state.panningOffset[1]; - const bottom = negativeHalfViewHeight / state.scaling[1]; // + state.panningOffset[1]; - - console.log('top', top, 'right', right, 'bottom', bottom, 'left', left); - - const leftAlign = -left; - const topAlign = (top - bottom) / 2; - - console.log('leftAlign', leftAlign, 'top align', topAlign); - - return ([worldX, worldY]) => [ - (worldX + state.panningOffset[0]) * (viewWidth / (right - left)) + leftAlign, - -(worldY + state.panningOffset[1]) * (viewHeight / (top - bottom)) + topAlign, - - /* - // should be centered - worldX * (viewWidth / (right - left)) - (left + right) / viewWidth, - worldY * (viewHeight / (top - bottom)) - (top + bottom) / viewHeight, - */ - /* - worldX * (state.rasterSize[0] / (right - left)) - (right + left) / (right - left), - worldY * (state.rasterSize[1] / (top - bottom)) - (top + bottom) / (top - bottom), - */ - ]; + const renderWidth = state.rasterSize[0]; + const renderHeight = state.rasterSize[1]; + const clippingPlaneRight = renderWidth / 2 / state.scaling[0]; + const clippingPlaneTop = renderHeight / 2 / state.scaling[1]; + const clippingPlaneLeft = -clippingPlaneRight; + const clippingPlaneBottom = -clippingPlaneTop; + + return ([worldX, worldY]) => { + const [xNdc, yNdc] = cameraToNdc( + worldX + state.panningOffset[0], + worldY + state.panningOffset[1], + clippingPlaneTop, + clippingPlaneRight, + clippingPlaneBottom, + clippingPlaneLeft + ); + + // ndc to raster + return [(renderWidth * (xNdc + 1)) / 2, (renderHeight * (-yNdc + 1)) / 2]; + }; }; +/** + * Adjust x, y to be bounded, in scale, of a clipping plane defined by top, right, bottom, left + * + * See explanation: + * https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix + */ +function cameraToNdc( + x: number, + y: number, + top: number, + right: number, + bottom: number, + left: number +): [number, number] { + const m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds + const m41 = -((right + left) / (right - left)); + + const m22 = 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds + const m42 = -((top + bottom) / (top - bottom)); + + const xPrime = x * m11 + m41; + const yPrime = y * m22 + m42; + + return [xPrime, yPrime]; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index 177ca674f7c0f0..53200f3c7f80fd 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -57,7 +57,7 @@ describe('worldToRaster', () => { expect(worldToRaster(store.getState())([0, 0])).toEqual([150, 100]); }); }); - describe('when the user has panned up and to the right by 50', () => { + describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { const action: UserPanned = { type: 'userPanned', payload: [-50, -50] }; store.dispatch(action); @@ -74,5 +74,31 @@ describe('worldToRaster', () => { ); }); }); + describe('when the user has panned to the right by 350 and up by 250', () => { + beforeEach(() => { + const action: UserPanned = { type: 'userPanned', payload: [-350, -250] }; + store.dispatch(action); + }); + describe('when the user has scaled to 2', () => { + // the viewport will only cover half, or 150x100 instead of 300x200 + beforeEach(() => { + const action: UserScaled = { type: 'userScaled', payload: [2, 2] }; + store.dispatch(action); + }); + // we expect the viewport to be + // minX = 350 - (150/2) = 275 + // maxX = 350 + (150/2) = 425 + // minY = 250 - (100/2) = 200 + // maxY = 250 + (100/2) = 300 + it('should convert 350,250 in world space to 150,100 (center) in raster space', () => { + expect(worldToRaster(store.getState())([350, 250]).toString()).toEqual( + [150, 100].toString() + ); + }); + it('should convert 275,300 in world space to 0,0 (top left) in raster space', () => { + expect(worldToRaster(store.getState())([275, 300]).toString()).toEqual([0, 0].toString()); + }); + }); + }); }); }); From 84474e32ba0e8b3842f06123b8e21eddebb5adb1 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 16:23:51 -0500 Subject: [PATCH 09/86] just about passing --- .../store/camera/raster_to_world.test.ts | 103 ++++++++++++++++++ .../resolver/store/camera/selectors.ts | 52 ++++++++- .../store/camera/world_to_raster.test.ts | 3 +- 3 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts new file mode 100644 index 00000000000000..126d197f7669dd --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Store, createStore } from 'redux'; +import { CameraAction, UserSetRasterSize, UserPanned, UserScaled } from './action'; +import { CameraState } from '../../types'; +import { cameraReducer } from './reducer'; +import { rasterToWorld } from './selectors'; + +describe('rasterToWorld', () => { + let store: Store; + beforeEach(() => { + store = createStore(cameraReducer, undefined); + }); + describe('when the raster size is 300 x 200 pixels', () => { + beforeEach(() => { + const action: UserSetRasterSize = { type: 'userSetRasterSize', payload: [300, 200] }; + store.dispatch(action); + }); + it('should convert 150,100 in raster space to 0,0 (center) in world space', () => { + expect(rasterToWorld(store.getState())([150, 100])).toEqual([0, 0]); + }); + it('should convert 150,0 in raster space to 0,100 (top) in world space', () => { + expect(rasterToWorld(store.getState())([150, 0])).toEqual([0, 100]); + }); + it('should convert 300,0 in raster space to 150,100 (top right) in world space', () => { + expect(rasterToWorld(store.getState())([300, 0])).toEqual([150, 100]); + }); + it('should convert 300,100 in raster space to 150,0 (right) in world space', () => { + expect(rasterToWorld(store.getState())([300, 100])).toEqual([150, 0]); + }); + it('should convert 300,200 in raster space to 150,-100 (right bottom) in world space', () => { + expect(rasterToWorld(store.getState())([300, 200])).toEqual([150, -100]); + }); + it('should convert 150,200 in raster space to 0,-100 (bottom) in world space', () => { + expect(rasterToWorld(store.getState())([150, 200])).toEqual([0, -100]); + }); + it('should convert 0,200 in raster space to -150,-100 (bottom left) in world space', () => { + expect(rasterToWorld(store.getState())([0, 200])).toEqual([-150, -100]); + }); + it('should convert 0,100 in raster space to -150,0 (left) in world space', () => { + expect(rasterToWorld(store.getState())([0, 100])).toEqual([-150, 0]); + }); + it('should convert 0,0 in raster space to -150,100 (top left) in world space', () => { + expect(rasterToWorld(store.getState())([0, 0])).toEqual([-150, 100]); + }); + describe('when the user has zoomed to 0.5', () => { + beforeEach(() => { + const action: UserScaled = { type: 'userScaled', payload: [0.5, 0.5] }; + store.dispatch(action); + }); + it('should convert 150, 100 (center) to 0, 0 (center) in world space', () => { + expect(rasterToWorld(store.getState())([150, 100])).toEqual([0, 0]); + }); + }); + describe('when the user has panned to the right and up by 50', () => { + beforeEach(() => { + const action: UserPanned = { type: 'userPanned', payload: [-50, -50] }; + store.dispatch(action); + }); + it('should convert 100,150 in raster space to 0,0 (center) in world space', () => { + expect(rasterToWorld(store.getState())([100, 150]).toString()).toEqual([0, 0].toString()); + }); + it('should convert 150,100 (center) in raster space to 50,50 (right and up a bit) in world space', () => { + expect(rasterToWorld(store.getState())([150, 100]).toString()).toEqual([50, 50].toString()); + }); + it('should convert 160,210 (center) in raster space to 60,-60 (right and down a bit) in world space', () => { + expect(rasterToWorld(store.getState())([160, 210]).toString()).toEqual( + [60, -60].toString() + ); + }); + }); + describe('when the user has panned to the right by 350 and up by 250', () => { + beforeEach(() => { + const action: UserPanned = { type: 'userPanned', payload: [-350, -250] }; + store.dispatch(action); + }); + describe('when the user has scaled to 2', () => { + // the viewport will only cover half, or 150x100 instead of 300x200 + beforeEach(() => { + const action: UserScaled = { type: 'userScaled', payload: [2, 2] }; + store.dispatch(action); + }); + // we expect the viewport to be + // minX = 350 - (150/2) = 275 + // maxX = 350 + (150/2) = 425 + // minY = 250 - (100/2) = 200 + // maxY = 250 + (100/2) = 300 + it('should convert 150,100 (center) in raster space to 350,250 in world space', () => { + expect(rasterToWorld(store.getState())([150, 100]).toString()).toEqual( + [350, 250].toString() + ); + }); + it('should convert 0,0 (top left) in raster space to 275,300 in world space', () => { + expect(rasterToWorld(store.getState())([0, 0]).toString()).toEqual([275, 300].toString()); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index cb5f0780cae2ed..962a5700348316 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -19,7 +19,7 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => const clippingPlaneBottom = -clippingPlaneTop; return ([worldX, worldY]) => { - const [xNdc, yNdc] = cameraToNdc( + const [xNdc, yNdc] = orthographicProjection( worldX + state.panningOffset[0], worldY + state.panningOffset[1], clippingPlaneTop, @@ -39,7 +39,7 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => * See explanation: * https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix */ -function cameraToNdc( +function orthographicProjection( x: number, y: number, top: number, @@ -58,3 +58,51 @@ function cameraToNdc( return [xPrime, yPrime]; } + +function inverseOrthographicProjection( + x: number, + y: number, + top: number, + right: number, + bottom: number, + left: number +): [number, number] { + const m11 = (right - left) / 2; + const m41 = (right + left) / (right - left); + + const m22 = (top - bottom) / 2; + const m42 = (top + bottom) / (top - bottom); + + const xPrime = x * m11 + m41; + const yPrime = y * m22 + m42; + + return [xPrime, yPrime]; +} + +export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { + const renderWidth = state.rasterSize[0]; + const renderHeight = state.rasterSize[1]; + const clippingPlaneRight = renderWidth / 2 / state.scaling[0]; + const clippingPlaneTop = renderHeight / 2 / state.scaling[1]; + const clippingPlaneLeft = -clippingPlaneRight; + const clippingPlaneBottom = -clippingPlaneTop; + + return ([rasterX, rasterY]) => { + // raster to ndc + const ndcX = (rasterX / renderWidth) * 2 - 1; + const ndcY = -1 * ((rasterY / renderHeight) * 2 - 1); + + const [panningTranslatedX, panningTranslatedY] = inverseOrthographicProjection( + ndcX, + ndcY, + clippingPlaneTop, + clippingPlaneRight, + clippingPlaneBottom, + clippingPlaneLeft + ); + return [ + panningTranslatedX - state.panningOffset[0], + panningTranslatedY - state.panningOffset[1], + ]; + }; +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index 53200f3c7f80fd..75814651c8ddf2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -23,7 +23,6 @@ describe('worldToRaster', () => { it('should convert 0,0 (center) in world space to 150,100 in raster space', () => { expect(worldToRaster(store.getState())([0, 0])).toEqual([150, 100]); }); - // top it('should convert 0,100 (top) in world space to 150,0 in raster space', () => { expect(worldToRaster(store.getState())([0, 100])).toEqual([150, 0]); }); @@ -45,7 +44,7 @@ describe('worldToRaster', () => { it('should convert -150,0 (left) in world space to 0,100 in raster space', () => { expect(worldToRaster(store.getState())([-150, 0])).toEqual([0, 100]); }); - it('should convert -150,100 (top left) in world space to 0,100 in raster space', () => { + it('should convert -150,100 (top left) in world space to 0,0 in raster space', () => { expect(worldToRaster(store.getState())([-150, 100])).toEqual([0, 0]); }); describe('when the user has zoomed to 0.5', () => { From 628055876a1cb6bd22995036907b3cb6c4e89b1d Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 16:32:09 -0500 Subject: [PATCH 10/86] removed tostring junk --- .../store/camera/raster_to_world.test.ts | 14 ++++------- .../store/camera/world_to_raster.test.ts | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts index 126d197f7669dd..ab4dc0897aed56 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts @@ -62,15 +62,13 @@ describe('rasterToWorld', () => { store.dispatch(action); }); it('should convert 100,150 in raster space to 0,0 (center) in world space', () => { - expect(rasterToWorld(store.getState())([100, 150]).toString()).toEqual([0, 0].toString()); + expect(rasterToWorld(store.getState())([100, 150])).toEqual([0, 0]); }); it('should convert 150,100 (center) in raster space to 50,50 (right and up a bit) in world space', () => { - expect(rasterToWorld(store.getState())([150, 100]).toString()).toEqual([50, 50].toString()); + expect(rasterToWorld(store.getState())([150, 100])).toEqual([50, 50]); }); it('should convert 160,210 (center) in raster space to 60,-60 (right and down a bit) in world space', () => { - expect(rasterToWorld(store.getState())([160, 210]).toString()).toEqual( - [60, -60].toString() - ); + expect(rasterToWorld(store.getState())([160, 210])).toEqual([60, -60]); }); }); describe('when the user has panned to the right by 350 and up by 250', () => { @@ -90,12 +88,10 @@ describe('rasterToWorld', () => { // minY = 250 - (100/2) = 200 // maxY = 250 + (100/2) = 300 it('should convert 150,100 (center) in raster space to 350,250 in world space', () => { - expect(rasterToWorld(store.getState())([150, 100]).toString()).toEqual( - [350, 250].toString() - ); + expect(rasterToWorld(store.getState())([150, 100])).toEqual([350, 250]); }); it('should convert 0,0 (top left) in raster space to 275,300 in world space', () => { - expect(rasterToWorld(store.getState())([0, 0]).toString()).toEqual([275, 300].toString()); + expect(rasterToWorld(store.getState())([0, 0])).toEqual([275, 300]); }); }); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index 75814651c8ddf2..a87e3e0cb6d0e9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -16,12 +16,19 @@ describe('worldToRaster', () => { store = createStore(cameraReducer, undefined); }); describe('when the raster size is 300 x 200 pixels', () => { + let compare: ( + worldPosition: [number, number], + expectedRasterPosition: [number, number] + ) => void; beforeEach(() => { const action: UserSetRasterSize = { type: 'userSetRasterSize', payload: [300, 200] }; store.dispatch(action); + compare = (worldPosition: [number, number], expectedRasterPosition: [number, number]) => { + expect(worldToRaster(store.getState())(worldPosition)).toEqual(expectedRasterPosition); + }; }); it('should convert 0,0 (center) in world space to 150,100 in raster space', () => { - expect(worldToRaster(store.getState())([0, 0])).toEqual([150, 100]); + compare([0, 0], [150, 100]); }); it('should convert 0,100 (top) in world space to 150,0 in raster space', () => { expect(worldToRaster(store.getState())([0, 100])).toEqual([150, 0]); @@ -62,15 +69,13 @@ describe('worldToRaster', () => { store.dispatch(action); }); it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { - expect(worldToRaster(store.getState())([0, 0]).toString()).toEqual([100, 150].toString()); + expect(worldToRaster(store.getState())([0, 0])).toEqual([100, 150]); }); it('should convert 50,50 (right and up a bit) in world space to 150,100 (center) in raster space', () => { - expect(worldToRaster(store.getState())([50, 50]).toString()).toEqual([150, 100].toString()); + expect(worldToRaster(store.getState())([50, 50])).toEqual([150, 100]); }); it('should convert 60,-60 (right and down a bit) in world space to 160,210 (center) in raster space', () => { - expect(worldToRaster(store.getState())([60, -60]).toString()).toEqual( - [160, 210].toString() - ); + expect(worldToRaster(store.getState())([60, -60])).toEqual([160, 210]); }); }); describe('when the user has panned to the right by 350 and up by 250', () => { @@ -90,12 +95,10 @@ describe('worldToRaster', () => { // minY = 250 - (100/2) = 200 // maxY = 250 + (100/2) = 300 it('should convert 350,250 in world space to 150,100 (center) in raster space', () => { - expect(worldToRaster(store.getState())([350, 250]).toString()).toEqual( - [150, 100].toString() - ); + expect(worldToRaster(store.getState())([350, 250])).toEqual([150, 100]); }); it('should convert 275,300 in world space to 0,0 (top left) in raster space', () => { - expect(worldToRaster(store.getState())([275, 300]).toString()).toEqual([0, 0].toString()); + expect(worldToRaster(store.getState())([275, 300])).toEqual([0, 0]); }); }); }); From 048429136a23d9fdebb41fea11ea1c0812c509d4 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 16:37:00 -0500 Subject: [PATCH 11/86] refactored tests to use toBeCloseTo --- .../store/camera/raster_to_world.test.ts | 36 +++++++++------- .../store/camera/world_to_raster.test.ts | 41 +++++++++---------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts index ab4dc0897aed56..8de1ff5fa91245 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts @@ -12,8 +12,14 @@ import { rasterToWorld } from './selectors'; describe('rasterToWorld', () => { let store: Store; + let compare: (worldPosition: [number, number], expectedRasterPosition: [number, number]) => void; beforeEach(() => { store = createStore(cameraReducer, undefined); + compare = (rasterPosition: [number, number], expectedWorldPosition: [number, number]) => { + const [worldX, worldY] = rasterToWorld(store.getState())(rasterPosition); + expect(worldX).toBeCloseTo(expectedWorldPosition[0]); + expect(worldY).toBeCloseTo(expectedWorldPosition[1]); + }; }); describe('when the raster size is 300 x 200 pixels', () => { beforeEach(() => { @@ -21,31 +27,31 @@ describe('rasterToWorld', () => { store.dispatch(action); }); it('should convert 150,100 in raster space to 0,0 (center) in world space', () => { - expect(rasterToWorld(store.getState())([150, 100])).toEqual([0, 0]); + compare([150, 100], [0, 0]); }); it('should convert 150,0 in raster space to 0,100 (top) in world space', () => { - expect(rasterToWorld(store.getState())([150, 0])).toEqual([0, 100]); + compare([150, 0], [0, 100]); }); it('should convert 300,0 in raster space to 150,100 (top right) in world space', () => { - expect(rasterToWorld(store.getState())([300, 0])).toEqual([150, 100]); + compare([300, 0], [150, 100]); }); it('should convert 300,100 in raster space to 150,0 (right) in world space', () => { - expect(rasterToWorld(store.getState())([300, 100])).toEqual([150, 0]); + compare([300, 100], [150, 0]); }); it('should convert 300,200 in raster space to 150,-100 (right bottom) in world space', () => { - expect(rasterToWorld(store.getState())([300, 200])).toEqual([150, -100]); + compare([300, 200], [150, -100]); }); it('should convert 150,200 in raster space to 0,-100 (bottom) in world space', () => { - expect(rasterToWorld(store.getState())([150, 200])).toEqual([0, -100]); + compare([150, 200], [0, -100]); }); it('should convert 0,200 in raster space to -150,-100 (bottom left) in world space', () => { - expect(rasterToWorld(store.getState())([0, 200])).toEqual([-150, -100]); + compare([0, 200], [-150, -100]); }); it('should convert 0,100 in raster space to -150,0 (left) in world space', () => { - expect(rasterToWorld(store.getState())([0, 100])).toEqual([-150, 0]); + compare([0, 100], [-150, 0]); }); it('should convert 0,0 in raster space to -150,100 (top left) in world space', () => { - expect(rasterToWorld(store.getState())([0, 0])).toEqual([-150, 100]); + compare([0, 0], [-150, 100]); }); describe('when the user has zoomed to 0.5', () => { beforeEach(() => { @@ -53,7 +59,7 @@ describe('rasterToWorld', () => { store.dispatch(action); }); it('should convert 150, 100 (center) to 0, 0 (center) in world space', () => { - expect(rasterToWorld(store.getState())([150, 100])).toEqual([0, 0]); + compare([150, 100], [0, 0]); }); }); describe('when the user has panned to the right and up by 50', () => { @@ -62,13 +68,13 @@ describe('rasterToWorld', () => { store.dispatch(action); }); it('should convert 100,150 in raster space to 0,0 (center) in world space', () => { - expect(rasterToWorld(store.getState())([100, 150])).toEqual([0, 0]); + compare([100, 150], [0, 0]); }); it('should convert 150,100 (center) in raster space to 50,50 (right and up a bit) in world space', () => { - expect(rasterToWorld(store.getState())([150, 100])).toEqual([50, 50]); + compare([150, 100], [50, 50]); }); it('should convert 160,210 (center) in raster space to 60,-60 (right and down a bit) in world space', () => { - expect(rasterToWorld(store.getState())([160, 210])).toEqual([60, -60]); + compare([160, 210], [60, -60]); }); }); describe('when the user has panned to the right by 350 and up by 250', () => { @@ -88,10 +94,10 @@ describe('rasterToWorld', () => { // minY = 250 - (100/2) = 200 // maxY = 250 + (100/2) = 300 it('should convert 150,100 (center) in raster space to 350,250 in world space', () => { - expect(rasterToWorld(store.getState())([150, 100])).toEqual([350, 250]); + compare([150, 100], [350, 250]); }); it('should convert 0,0 (top left) in raster space to 275,300 in world space', () => { - expect(rasterToWorld(store.getState())([0, 0])).toEqual([275, 300]); + compare([0, 0], [275, 300]); }); }); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index a87e3e0cb6d0e9..3651ddf851030e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -12,47 +12,46 @@ import { worldToRaster } from './selectors'; describe('worldToRaster', () => { let store: Store; + let compare: (worldPosition: [number, number], expectedRasterPosition: [number, number]) => void; beforeEach(() => { store = createStore(cameraReducer, undefined); + compare = (worldPosition: [number, number], expectedRasterPosition: [number, number]) => { + const [rasterX, rasterY] = worldToRaster(store.getState())(worldPosition); + expect(rasterX).toBeCloseTo(expectedRasterPosition[0]); + expect(rasterY).toBeCloseTo(expectedRasterPosition[1]); + }; }); describe('when the raster size is 300 x 200 pixels', () => { - let compare: ( - worldPosition: [number, number], - expectedRasterPosition: [number, number] - ) => void; beforeEach(() => { const action: UserSetRasterSize = { type: 'userSetRasterSize', payload: [300, 200] }; store.dispatch(action); - compare = (worldPosition: [number, number], expectedRasterPosition: [number, number]) => { - expect(worldToRaster(store.getState())(worldPosition)).toEqual(expectedRasterPosition); - }; }); it('should convert 0,0 (center) in world space to 150,100 in raster space', () => { compare([0, 0], [150, 100]); }); it('should convert 0,100 (top) in world space to 150,0 in raster space', () => { - expect(worldToRaster(store.getState())([0, 100])).toEqual([150, 0]); + compare([0, 100], [150, 0]); }); it('should convert 150,100 (top right) in world space to 300,0 in raster space', () => { - expect(worldToRaster(store.getState())([150, 100])).toEqual([300, 0]); + compare([150, 100], [300, 0]); }); it('should convert 150,0 (right) in world space to 300,100 in raster space', () => { - expect(worldToRaster(store.getState())([150, 0])).toEqual([300, 100]); + compare([150, 0], [300, 100]); }); it('should convert 150,-100 (right bottom) in world space to 300,200 in raster space', () => { - expect(worldToRaster(store.getState())([150, -100])).toEqual([300, 200]); + compare([150, -100], [300, 200]); }); it('should convert 0,-100 (bottom) in world space to 150,200 in raster space', () => { - expect(worldToRaster(store.getState())([0, -100])).toEqual([150, 200]); + compare([0, -100], [150, 200]); }); it('should convert -150,-100 (bottom left) in world space to 0,200 in raster space', () => { - expect(worldToRaster(store.getState())([-150, -100])).toEqual([0, 200]); + compare([-150, -100], [0, 200]); }); it('should convert -150,0 (left) in world space to 0,100 in raster space', () => { - expect(worldToRaster(store.getState())([-150, 0])).toEqual([0, 100]); + compare([-150, 0], [0, 100]); }); it('should convert -150,100 (top left) in world space to 0,0 in raster space', () => { - expect(worldToRaster(store.getState())([-150, 100])).toEqual([0, 0]); + compare([-150, 100], [0, 0]); }); describe('when the user has zoomed to 0.5', () => { beforeEach(() => { @@ -60,7 +59,7 @@ describe('worldToRaster', () => { store.dispatch(action); }); it('should convert 0, 0 (center) in world space to 150, 100 (center)', () => { - expect(worldToRaster(store.getState())([0, 0])).toEqual([150, 100]); + compare([0, 0], [150, 100]); }); }); describe('when the user has panned to the right and up by 50', () => { @@ -69,13 +68,13 @@ describe('worldToRaster', () => { store.dispatch(action); }); it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { - expect(worldToRaster(store.getState())([0, 0])).toEqual([100, 150]); + compare([0, 0], [100, 150]); }); it('should convert 50,50 (right and up a bit) in world space to 150,100 (center) in raster space', () => { - expect(worldToRaster(store.getState())([50, 50])).toEqual([150, 100]); + compare([50, 50], [150, 100]); }); it('should convert 60,-60 (right and down a bit) in world space to 160,210 (center) in raster space', () => { - expect(worldToRaster(store.getState())([60, -60])).toEqual([160, 210]); + compare([60, -60], [160, 210]); }); }); describe('when the user has panned to the right by 350 and up by 250', () => { @@ -95,10 +94,10 @@ describe('worldToRaster', () => { // minY = 250 - (100/2) = 200 // maxY = 250 + (100/2) = 300 it('should convert 350,250 in world space to 150,100 (center) in raster space', () => { - expect(worldToRaster(store.getState())([350, 250])).toEqual([150, 100]); + compare([350, 250], [150, 100]); }); it('should convert 275,300 in world space to 0,0 (top left) in raster space', () => { - expect(worldToRaster(store.getState())([275, 300])).toEqual([0, 0]); + compare([275, 300], [0, 0]); }); }); }); From dadcb6d1c001cb8a886d750728ec5361c376a95a Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 17:43:00 -0500 Subject: [PATCH 12/86] fix issues --- .../embeddables/resolver/embeddable.tsx | 4 +- .../resolver/store/camera/selectors.ts | 2 + .../embeddables/resolver/store/index.ts | 16 +++++-- .../embeddables/resolver/store/selectors.ts | 4 ++ .../embeddables/resolver/view/index.tsx | 48 +++++++++++++++++-- 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index 0e7881223c0947..35b995848a69a8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; import ReactDOM from 'react-dom'; import React from 'react'; import { AppRoot } from './view'; @@ -14,6 +13,7 @@ import { IContainer, Embeddable, } from '../../../../../../src/plugins/embeddable/public'; +import { HttpServiceBase } from '../../../../../../src/core/public'; export class ResolverEmbeddable extends Embeddable { public readonly type = 'resolver'; @@ -42,7 +42,7 @@ export class ResolverEmbeddable extends Embeddable { } this.lastRenderTarget = node; // TODO, figure out how to destroy middleware - const { store } = storeFactory({ httpServiceBase }); + const { store } = storeFactory({ httpServiceBase: this.httpServiceBase }); ReactDOM.render(, node); } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 962a5700348316..91085b1a8237f5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -106,3 +106,5 @@ export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => ]; }; }; + +export const scale = (state: CameraState): Vector2 => state.scaling; 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 9c759dfcf1c3d7..89ebeb15d90535 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -4,12 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore } from 'redux'; -import { HttpServiceBase } from 'kibana/public'; +import { createStore, StoreEnhancer } from 'redux'; import { resolverReducer } from './reducer'; +import { HttpServiceBase } from '../../../../../../../src/core/public'; export const storeFactory = ({ httpServiceBase }: { httpServiceBase: HttpServiceBase }) => { - const store = createStore(resolverReducer, undefined, applyMiddleware()); + interface SomethingThatMightHaveReduxDevTools { + __REDUX_DEVTOOLS_EXTENSION__?: (options?: { name?: string }) => StoreEnhancer; + } + const windowWhichMightHaveReduxDevTools = window as SomethingThatMightHaveReduxDevTools; + const store = createStore( + resolverReducer, + windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__ && + windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__({ + name: 'Resolver', + }) + ); return { store, }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index b57a502837a6a8..f3e14e53728814 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -9,6 +9,10 @@ import { ResolverState } from '../types'; export const worldToRaster = composeSelectors(cameraStateSelector, cameraSelectors.worldToRaster); +export const rasterToWorld = composeSelectors(cameraStateSelector, cameraSelectors.rasterToWorld); + +export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale); + function cameraStateSelector(state: ResolverState) { return state.camera; } 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 932261094a6035..58385f40911adf 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { MouseEventHandler, useCallback, useState, useEffect } from 'react'; import { Store } from 'redux'; -import { Provider, useSelector } from 'react-redux'; +import { Provider, useSelector, useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; import { ResolverState, ResolverAction } from '../types'; import * as selectors from '../store/selectors'; @@ -20,7 +21,48 @@ export const AppRoot: React.FC<{ ); }); +const useResolverDispatch = () => useDispatch>(); + const Diagnostic: React.FC<{}> = React.memo(() => { const worldToRaster = useSelector(selectors.worldToRaster); - return
frig
; + + console.log('we rerendered!', 'worldToRaster([0, 0])', worldToRaster([0, 0])); + const scale = useSelector(selectors.scale); + + const [elementBoundingClientRect, clientRectCallbackFunction] = useClientRect(); + + const dispatch = useResolverDispatch(); + + useEffect(() => { + if (elementBoundingClientRect !== undefined) { + dispatch({ + type: 'userSetRasterSize', + payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], + }); + } + }, [elementBoundingClientRect, dispatch]); + + const handleMouseMove: MouseEventHandler = useCallback( + event => { + if (event.buttons === 1) { + dispatch({ + type: 'userPanned', + payload: [event.movementX * scale[0], event.movementY * scale[1]], + }); + } + }, + [scale, dispatch] + ); + + return
; }); + +function useClientRect() { + const [rect, setRect] = useState(); + const ref = useCallback((node: Element | null) => { + if (node !== null) { + setRect(node.getBoundingClientRect()); + } + }, []); + return [rect, ref] as const; +} From 2caea4222d317cc40e7deec71d544eaa103de354 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 18:09:47 -0500 Subject: [PATCH 13/86] maybe panning is working? hard to know --- .../resolver/store/camera/reducer.ts | 5 +- .../embeddables/resolver/view/index.tsx | 77 +++++++++++------ .../applications/resolver_test/index.tsx | 84 ++++++++++++------- 3 files changed, 107 insertions(+), 59 deletions(-) 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 70a18f527b433f..e4ca9a461461d6 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 @@ -26,7 +26,10 @@ export const cameraReducer: Reducer = ( } else if (action.type === 'userPanned') { return { ...state, - panningOffset: action.payload, + panningOffset: [ + state.panningOffset[0] + action.payload[0], + state.panningOffset[1] + action.payload[1], + ], }; } else if (action.type === 'userSetRasterSize') { 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 58385f40911adf..68eae64a0dc742 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { MouseEventHandler, useCallback, useState, useEffect } from 'react'; +import React, { MouseEventHandler, useCallback, useState, useEffect, useRef } from 'react'; import { Store } from 'redux'; import { Provider, useSelector, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; +import styled from 'styled-components'; import { ResolverState, ResolverAction } from '../types'; import * as selectors from '../store/selectors'; @@ -23,46 +24,68 @@ export const AppRoot: React.FC<{ const useResolverDispatch = () => useDispatch>(); -const Diagnostic: React.FC<{}> = React.memo(() => { - const worldToRaster = useSelector(selectors.worldToRaster); +const Diagnostic = styled( + React.memo(({ className }: { className?: string }) => { + const worldToRaster = useSelector(selectors.worldToRaster); - console.log('we rerendered!', 'worldToRaster([0, 0])', worldToRaster([0, 0])); - const scale = useSelector(selectors.scale); + console.log('we rerendered!', 'worldToRaster([0, 0])', worldToRaster([0, 0])); + const scale = useSelector(selectors.scale); - const [elementBoundingClientRect, clientRectCallbackFunction] = useClientRect(); + const [elementBoundingClientRect, clientRectCallbackFunction] = useAutoUpdatingClientRect(); - const dispatch = useResolverDispatch(); + const dispatch = useResolverDispatch(); - useEffect(() => { - if (elementBoundingClientRect !== undefined) { - dispatch({ - type: 'userSetRasterSize', - payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], - }); - } - }, [elementBoundingClientRect, dispatch]); - - const handleMouseMove: MouseEventHandler = useCallback( - event => { - if (event.buttons === 1) { + useEffect(() => { + if (elementBoundingClientRect !== undefined) { dispatch({ - type: 'userPanned', - payload: [event.movementX * scale[0], event.movementY * scale[1]], + type: 'userSetRasterSize', + payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], }); } - }, - [scale, dispatch] - ); + }, [elementBoundingClientRect, dispatch]); - return
; -}); + const handleMouseMove: MouseEventHandler = useCallback( + event => { + if (event.buttons === 1) { + dispatch({ + type: 'userPanned', + payload: [event.movementX * scale[0], event.movementY * scale[1]], + }); + } + }, + [scale, dispatch] + ); + + return ( +
+ ); + }) +)` + display: flex; + flex-grow: 1; +`; -function useClientRect() { +function useAutoUpdatingClientRect() { const [rect, setRect] = useState(); + const nodeRef = useRef(); + const ref = useCallback((node: Element | null) => { + nodeRef.current = node === null ? undefined : node; if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); + + useEffect(() => { + window.addEventListener('resize', handler, { passive: true }); + return () => { + window.removeEventListener('resize', handler); + }; + function handler() { + if (nodeRef.current !== undefined) { + setRect(nodeRef.current.getBoundingClientRect()); + } + } + }, []); return [rect, ref] as const; } diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index 98baad6a184119..f58aad813f3e96 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -10,6 +10,7 @@ import { AppMountParameters } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; import { IEmbeddable } from 'src/plugins/embeddable/public'; import { useEffect } from 'react'; +import styled from 'styled-components'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -18,6 +19,10 @@ export function renderApp( { element }: AppMountParameters, embeddable: Promise ) { + // TODO, is this right? + element.style.display = 'flex'; + element.style.flexGrow = '1'; + ReactDOM.render( @@ -30,34 +35,51 @@ export function renderApp( }; } -const AppRoot = React.memo( - ({ embeddable: embeddablePromise }: { embeddable: Promise }) => { - const [embeddable, setEmbeddable] = React.useState(undefined); - const [renderTarget, setRenderTarget] = React.useState(null); - - useEffect(() => { - let cleanUp; - Promise.race([ - new Promise((_resolve, reject) => { - cleanUp = reject; - }), - embeddablePromise, - ]).then(value => { - setEmbeddable(value); - }); - - return cleanUp; - }, [embeddablePromise]); - - useEffect(() => { - if (embeddable && renderTarget) { - embeddable.render(renderTarget); - return () => { - embeddable.destroy(); - }; - } - }, [embeddable, renderTarget]); - - return
; - } -); +const AppRoot = styled( + React.memo( + ({ + embeddable: embeddablePromise, + className, + }: { + embeddable: Promise; + className?: string; + }) => { + const [embeddable, setEmbeddable] = React.useState(undefined); + const [renderTarget, setRenderTarget] = React.useState(null); + + useEffect(() => { + let cleanUp; + Promise.race([ + new Promise((_resolve, reject) => { + cleanUp = reject; + }), + embeddablePromise, + ]).then(value => { + setEmbeddable(value); + }); + + return cleanUp; + }, [embeddablePromise]); + + useEffect(() => { + if (embeddable && renderTarget) { + embeddable.render(renderTarget); + return () => { + embeddable.destroy(); + }; + } + }, [embeddable, renderTarget]); + + return ( +
+ ); + } + ) +)` + display: flex; + flex-grow: 1; +`; From 1a193a91cbe3315b69dc515c862d12fa912b1fcb Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 18:25:57 -0500 Subject: [PATCH 14/86] pan around a bit --- .../embeddables/resolver/view/index.tsx | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) 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 68eae64a0dc742..50fd1cfac50f4d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { MouseEventHandler, useCallback, useState, useEffect, useRef } from 'react'; +import React, { MouseEventHandler, useCallback, useState, useEffect, useRef, useMemo } from 'react'; import { Store } from 'redux'; import { Provider, useSelector, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import styled from 'styled-components'; -import { ResolverState, ResolverAction } from '../types'; +import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; export const AppRoot: React.FC<{ @@ -26,9 +26,6 @@ const useResolverDispatch = () => useDispatch>(); const Diagnostic = styled( React.memo(({ className }: { className?: string }) => { - const worldToRaster = useSelector(selectors.worldToRaster); - - console.log('we rerendered!', 'worldToRaster([0, 0])', worldToRaster([0, 0])); const scale = useSelector(selectors.scale); const [elementBoundingClientRect, clientRectCallbackFunction] = useAutoUpdatingClientRect(); @@ -49,7 +46,7 @@ const Diagnostic = styled( if (event.buttons === 1) { dispatch({ type: 'userPanned', - payload: [event.movementX * scale[0], event.movementY * scale[1]], + payload: [event.movementX * scale[0], -event.movementY * scale[1]], }); } }, @@ -57,12 +54,15 @@ const Diagnostic = styled( ); return ( -
+
+ [0, 0], [])} /> +
); }) )` display: flex; flex-grow: 1; + position: relative; `; function useAutoUpdatingClientRect() { @@ -89,3 +89,26 @@ function useAutoUpdatingClientRect() { }, []); return [rect, ref] as const; } + +const DiagnosticDot = styled( + React.memo(({ className, worldPosition }: { className?: string; worldPosition: Vector2 }) => { + const worldToRaster = useSelector(selectors.worldToRaster); + const [left, top] = worldToRaster(worldPosition); + const style = { + left: (left - 30).toString() + 'px', + top: (top - 30).toString() + 'px', + }; + return ( + + 🤤 + + ); + }) +)` + position: absolute; + width: 60px; + height: 60px; + text-align: center; + font-size: 60px; + user-select: none; +`; From 138fe88bea97a58aa21c67dfb1459b5fb4d98fcf Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 20:19:23 -0500 Subject: [PATCH 15/86] add wheel zoom, but zoom isn't working --- .../public/embeddables/resolver/lib/math.ts | 9 ++++++ .../resolver/store/camera/reducer.ts | 6 +++- .../embeddables/resolver/view/index.tsx | 29 +++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts new file mode 100644 index 00000000000000..08c39a5baa6a07 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function clamp(value: number, minimum: number, maximum: number) { + return Math.max(Math.min(value, maximum), minimum); +} 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 e4ca9a461461d6..1c275e68295d0c 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 @@ -3,7 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { Reducer } from 'redux'; +import { clamp } from '../../lib/math'; + import { CameraState, ResolverAction } from '../../types'; function initialState(): CameraState { @@ -19,9 +22,10 @@ export const cameraReducer: Reducer = ( action ) => { if (action.type === 'userScaled') { + const [deltaX, deltaY] = action.payload; return { ...state, - scaling: action.payload, + scaling: [clamp(deltaX, 0.48, 1.2), clamp(deltaY, 0.48, 1.2)], }; } else if (action.type === 'userPanned') { 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 50fd1cfac50f4d..1dc117252a44f0 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -11,6 +11,7 @@ import { Dispatch } from 'redux'; import styled from 'styled-components'; import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; +import { clamp } from '../lib/math.ts'; export const AppRoot: React.FC<{ store: Store; @@ -26,6 +27,8 @@ const useResolverDispatch = () => useDispatch>(); const Diagnostic = styled( React.memo(({ className }: { className?: string }) => { + type ComponentElement = HTMLDivElement; + const scale = useSelector(selectors.scale); const [elementBoundingClientRect, clientRectCallbackFunction] = useAutoUpdatingClientRect(); @@ -41,7 +44,7 @@ const Diagnostic = styled( } }, [elementBoundingClientRect, dispatch]); - const handleMouseMove: MouseEventHandler = useCallback( + const handleMouseMove: MouseEventHandler = useCallback( event => { if (event.buttons === 1) { dispatch({ @@ -53,8 +56,30 @@ const Diagnostic = styled( [scale, dispatch] ); + const handleWheel = useCallback( + (event: React.WheelEvent) => { + if (event.ctrlKey) { + /* + * Clamp amount at ±10 percent per action. + * Determining the scale of the deltaY is difficult due to differences in UA. + */ + const scaleDelta = clamp(event.deltaY, -0.1, 0.1); + dispatch({ + type: 'userScaled', + payload: [scale[0] + scaleDelta, scale[1] + scaleDelta], + }); + } + }, + [scale, dispatch] + ); + return ( -
+
[0, 0], [])} />
); From c54ed96ca6cf2ebabda6faa303eadee392663336 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 3 Dec 2019 20:59:40 -0500 Subject: [PATCH 16/86] stopped using the delta panning but it obviously didnt help. would it have worked? so much simpler :/ --- .../public/embeddables/resolver/actions.ts | 1 - .../resolver/store/camera/action.ts | 37 +++++++++++--- .../store/camera/raster_to_world.test.ts | 6 +-- .../resolver/store/camera/reducer.ts | 49 +++++++++++++++++-- .../resolver/store/camera/selectors.ts | 28 ++++++++--- .../store/camera/world_to_raster.test.ts | 9 ++-- .../embeddables/resolver/store/selectors.ts | 2 + .../public/embeddables/resolver/types.ts | 9 +++- .../embeddables/resolver/view/index.tsx | 39 +++++++++++---- 9 files changed, 144 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts index ec0e875bbef0fc..a7297b05d002ca 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts @@ -6,4 +6,3 @@ import { CameraAction } from './store/camera'; export type ResolverAction = CameraAction; - 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 4c705201f3ec1b..ab6c15e35ed347 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 @@ -6,19 +6,44 @@ import { Vector2 } from '../../types'; -export interface UserScaled { +interface UserScaled { readonly type: 'userScaled'; readonly payload: Vector2; } -export interface UserPanned { - readonly type: 'userPanned'; +interface UserSetRasterSize { + readonly type: 'userSetRasterSize'; readonly payload: Vector2; } -export interface UserSetRasterSize { - readonly type: 'userSetRasterSize'; +interface UserSetPanningOffset { + readonly type: 'userSetPanningOffset'; + readonly payload: Vector2; +} + +interface UserStartedPanning { + readonly type: 'userStartedPanning'; + readonly payload: Vector2; +} + +interface UserContinuedPanning { + readonly type: 'userContinuedPanning'; readonly payload: Vector2; } -export type CameraAction = UserScaled | UserPanned | UserSetRasterSize; +interface UserStoppedPanning { + readonly type: 'userStoppedPanning'; +} + +interface UserCanceledPanning { + readonly type: 'userCanceledPanning'; +} + +export type CameraAction = + | UserScaled + | UserSetRasterSize + | UserSetPanningOffset + | UserStartedPanning + | UserContinuedPanning + | UserStoppedPanning + | UserCanceledPanning; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts index 8de1ff5fa91245..62491f7b539631 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts @@ -5,7 +5,7 @@ */ import { Store, createStore } from 'redux'; -import { CameraAction, UserSetRasterSize, UserPanned, UserScaled } from './action'; +import { CameraAction, UserSetRasterSize, UserSetPanningOffset, UserScaled } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; import { rasterToWorld } from './selectors'; @@ -64,7 +64,7 @@ describe('rasterToWorld', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: UserPanned = { type: 'userPanned', payload: [-50, -50] }; + const action: UserSetPanningOffset = { type: 'userPanned', payload: [-50, -50] }; store.dispatch(action); }); it('should convert 100,150 in raster space to 0,0 (center) in world space', () => { @@ -79,7 +79,7 @@ describe('rasterToWorld', () => { }); describe('when the user has panned to the right by 350 and up by 250', () => { beforeEach(() => { - const action: UserPanned = { type: 'userPanned', payload: [-350, -250] }; + const action: UserSetPanningOffset = { type: 'userPanned', payload: [-350, -250] }; store.dispatch(action); }); describe('when the user has scaled to 2', () => { 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 1c275e68295d0c..e4f3df408f4a90 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 @@ -5,6 +5,7 @@ */ import { Reducer } from 'redux'; +import { userIsPanning } from './selectors'; import { clamp } from '../../lib/math'; import { CameraState, ResolverAction } from '../../types'; @@ -12,8 +13,10 @@ import { CameraState, ResolverAction } from '../../types'; function initialState(): CameraState { return { scaling: [1, 1] as const, - panningOffset: [0, 0] as const, rasterSize: [0, 0] as const, + translation: [0, 0] as const, + panningOrigin: null, + currentPanningOffset: null, }; } @@ -27,14 +30,50 @@ export const cameraReducer: Reducer = ( ...state, scaling: [clamp(deltaX, 0.48, 1.2), clamp(deltaY, 0.48, 1.2)], }; - } else if (action.type === 'userPanned') { + } else if (action.type === 'userSetPanningOffset') { return { ...state, - panningOffset: [ - state.panningOffset[0] + action.payload[0], - state.panningOffset[1] + action.payload[1], + translation: [ + state.translation[0] + action.payload[0], + state.translation[1] + action.payload[1], ], }; + } else if (action.type === 'userStartedPanning') { + return { + ...state, + panningOrigin: action.payload, + currentPanningOffset: action.payload, + }; + } else if (action.type === 'userContinuedPanning') { + if (userIsPanning(state)) { + return { + ...state, + currentPanningOffset: action.payload, + }; + } else { + return state; + } + } else if (action.type === 'userStoppedPanning') { + if (userIsPanning(state)) { + // TODO, write some vector2 libs plz + const panningDeltaX = state.currentPanningOffset[0] - state.panningOrigin[0]; + const panningDeltaY = state.currentPanningOffset[1] - state.panningOrigin[1]; + + return { + ...state, + translation: [state.translation[0] + panningDeltaX, state.translation[1] + panningDeltaY], + panningOrigin: null, + currentPanningOffset: null, + }; + } else { + return state; + } + } else if (action.type === 'userCanceledPanning') { + return { + ...state, + panningOrigin: null, + currentPanningOffset: null, + }; } else if (action.type === 'userSetRasterSize') { return { ...state, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 91085b1a8237f5..1cda199278f0e3 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Vector2, CameraState } from '../../types'; +import { Vector2, CameraState, CameraStateWhenPanning } from '../../types'; /** * https://en.wikipedia.org/wiki/Orthographic_projection @@ -19,9 +19,10 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => const clippingPlaneBottom = -clippingPlaneTop; return ([worldX, worldY]) => { + const [translationX, translationY] = translationIncludingActivePanning(state); const [xNdc, yNdc] = orthographicProjection( - worldX + state.panningOffset[0], - worldY + state.panningOffset[1], + worldX + translationX, + worldY + translationY, clippingPlaneTop, clippingPlaneRight, clippingPlaneBottom, @@ -33,6 +34,19 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => }; }; +function translationIncludingActivePanning(state: CameraState): Vector2 { + if (userIsPanning(state)) { + const panningDeltaX = state.currentPanningOffset[0] - state.panningOrigin[0]; + const panningDeltaY = state.currentPanningOffset[1] - state.panningOrigin[1]; + return [ + state.translation[0] + panningDeltaX * state.scaling[0], + state.translation[1] + panningDeltaY * state.scaling[1], + ]; + } else { + return state.translation; + } +} + /** * Adjust x, y to be bounded, in scale, of a clipping plane defined by top, right, bottom, left * @@ -100,11 +114,11 @@ export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => clippingPlaneBottom, clippingPlaneLeft ); - return [ - panningTranslatedX - state.panningOffset[0], - panningTranslatedY - state.panningOffset[1], - ]; + return [panningTranslatedX - state.translation[0], panningTranslatedY - state.translation[1]]; }; }; export const scale = (state: CameraState): Vector2 => state.scaling; + +export const userIsPanning = (state: CameraState): state is CameraStateWhenPanning => + state.panningOrigin !== null; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index 3651ddf851030e..1d89075c3b0613 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -5,7 +5,7 @@ */ import { Store, createStore } from 'redux'; -import { CameraAction, UserSetRasterSize, UserPanned, UserScaled } from './action'; +import { CameraAction, UserSetRasterSize, UserSetPanningOffset, UserScaled } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; import { worldToRaster } from './selectors'; @@ -64,7 +64,7 @@ describe('worldToRaster', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: UserPanned = { type: 'userPanned', payload: [-50, -50] }; + const action: UserSetPanningOffset = { type: 'userSetPanningOffset', payload: [-50, -50] }; store.dispatch(action); }); it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { @@ -79,7 +79,10 @@ describe('worldToRaster', () => { }); describe('when the user has panned to the right by 350 and up by 250', () => { beforeEach(() => { - const action: UserPanned = { type: 'userPanned', payload: [-350, -250] }; + const action: UserSetPanningOffset = { + type: 'userSetPanningOffset', + payload: [-350, -250], + }; store.dispatch(action); }); describe('when the user has scaled to 2', () => { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index f3e14e53728814..c78092570c5bb2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -13,6 +13,8 @@ export const rasterToWorld = composeSelectors(cameraStateSelector, cameraSelecto export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale); +export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelectors.userIsPanning); + function cameraStateSelector(state: ResolverState) { return state.camera; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 8c275f67fc464f..7ffcd21f02542a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -11,8 +11,15 @@ export { ResolverAction } from './actions'; export interface CameraState { readonly scaling: Vector2; - readonly panningOffset: Vector2; readonly rasterSize: Vector2; + readonly translation: Vector2; + readonly panningOrigin: Vector2 | null; + readonly currentPanningOffset: Vector2 | null; +} + +export interface CameraStateWhenPanning extends CameraState { + readonly panningOrigin: Vector2; + readonly currentPanningOffset: Vector2; } export type Vector2 = readonly [number, number]; 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 1dc117252a44f0..7dca41e649b1f9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { MouseEventHandler, useCallback, useState, useEffect, useRef, useMemo } from 'react'; +import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react'; import { Store } from 'redux'; import { Provider, useSelector, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import styled from 'styled-components'; import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; -import { clamp } from '../lib/math.ts'; +import { clamp } from '../lib/math'; export const AppRoot: React.FC<{ store: Store; @@ -27,9 +27,8 @@ const useResolverDispatch = () => useDispatch>(); const Diagnostic = styled( React.memo(({ className }: { className?: string }) => { - type ComponentElement = HTMLDivElement; - const scale = useSelector(selectors.scale); + const userIsPanning = useSelector(selectors.userIsPanning); const [elementBoundingClientRect, clientRectCallbackFunction] = useAutoUpdatingClientRect(); @@ -44,18 +43,36 @@ const Diagnostic = styled( } }, [elementBoundingClientRect, dispatch]); - const handleMouseMove: MouseEventHandler = useCallback( - event => { - if (event.buttons === 1) { + const handleMouseDown = useCallback( + (event: React.MouseEvent) => { + dispatch({ + type: 'userStartedPanning', + payload: [event.clientX, -event.clientY], + }); + }, + [dispatch] + ); + + const handleMouseMove = useCallback( + (event: React.MouseEvent) => { + if (event.buttons === 1 && userIsPanning) { dispatch({ - type: 'userPanned', - payload: [event.movementX * scale[0], -event.movementY * scale[1]], + type: 'userContinuedPanning', + payload: [event.clientX, -event.clientY], }); } }, - [scale, dispatch] + [dispatch, userIsPanning] ); + const handleMouseUp = useCallback(() => { + if (userIsPanning) { + dispatch({ + type: 'userStoppedPanning', + }); + } + }, [dispatch, userIsPanning]); + const handleWheel = useCallback( (event: React.WheelEvent) => { if (event.ctrlKey) { @@ -79,6 +96,8 @@ const Diagnostic = styled( ref={clientRectCallbackFunction} onMouseMove={handleMouseMove} onWheel={handleWheel} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} > [0, 0], [])} />
From d3c8b1d656f3f4b29db9223bcbbfbfe07dafa6e9 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 09:56:24 -0500 Subject: [PATCH 17/86] worldToRaster tests were failing because we limited the zoom --- .../embeddables/resolver/store/camera/reducer.ts | 7 ++----- .../resolver/store/camera/world_to_raster.test.ts | 15 +++++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) 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 e4f3df408f4a90..b35d20e0cd59f8 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 @@ -28,15 +28,12 @@ export const cameraReducer: Reducer = ( const [deltaX, deltaY] = action.payload; return { ...state, - scaling: [clamp(deltaX, 0.48, 1.2), clamp(deltaY, 0.48, 1.2)], + scaling: [clamp(deltaX, 0.1, 3), clamp(deltaY, 0.1, 3)], }; } else if (action.type === 'userSetPanningOffset') { return { ...state, - translation: [ - state.translation[0] + action.payload[0], - state.translation[1] + action.payload[1], - ], + translation: action.payload, }; } else if (action.type === 'userStartedPanning') { return { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index 1d89075c3b0613..ad3da4dc89773d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -5,7 +5,7 @@ */ import { Store, createStore } from 'redux'; -import { CameraAction, UserSetRasterSize, UserSetPanningOffset, UserScaled } from './action'; +import { CameraAction } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; import { worldToRaster } from './selectors'; @@ -23,7 +23,7 @@ describe('worldToRaster', () => { }); describe('when the raster size is 300 x 200 pixels', () => { beforeEach(() => { - const action: UserSetRasterSize = { type: 'userSetRasterSize', payload: [300, 200] }; + const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] }; store.dispatch(action); }); it('should convert 0,0 (center) in world space to 150,100 in raster space', () => { @@ -55,7 +55,7 @@ describe('worldToRaster', () => { }); describe('when the user has zoomed to 0.5', () => { beforeEach(() => { - const action: UserScaled = { type: 'userScaled', payload: [0.5, 0.5] }; + const action: CameraAction = { type: 'userScaled', payload: [0.5, 0.5] }; store.dispatch(action); }); it('should convert 0, 0 (center) in world space to 150, 100 (center)', () => { @@ -64,7 +64,7 @@ describe('worldToRaster', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: UserSetPanningOffset = { type: 'userSetPanningOffset', payload: [-50, -50] }; + const action: CameraAction = { type: 'userSetPanningOffset', payload: [-50, -50] }; store.dispatch(action); }); it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { @@ -79,16 +79,19 @@ describe('worldToRaster', () => { }); describe('when the user has panned to the right by 350 and up by 250', () => { beforeEach(() => { - const action: UserSetPanningOffset = { + const action: CameraAction = { type: 'userSetPanningOffset', payload: [-350, -250], }; store.dispatch(action); }); + it('should convert 350,250 in world space to 150,100 (center) in raster space', () => { + compare([350, 250], [150, 100]); + }); describe('when the user has scaled to 2', () => { // the viewport will only cover half, or 150x100 instead of 300x200 beforeEach(() => { - const action: UserScaled = { type: 'userScaled', payload: [2, 2] }; + const action: CameraAction = { type: 'userScaled', payload: [2, 2] }; store.dispatch(action); }); // we expect the viewport to be From 16385518d88849c44a8e51dd9d3dec02189ad47b Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 10:15:03 -0500 Subject: [PATCH 18/86] fixed tests --- .../store/camera/raster_to_world.test.ts | 12 +- .../resolver/store/camera/selectors.ts | 108 ++++++++++++------ 2 files changed, 76 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts index 62491f7b539631..d0dd649efa95d5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts @@ -5,7 +5,7 @@ */ import { Store, createStore } from 'redux'; -import { CameraAction, UserSetRasterSize, UserSetPanningOffset, UserScaled } from './action'; +import { CameraAction } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; import { rasterToWorld } from './selectors'; @@ -23,7 +23,7 @@ describe('rasterToWorld', () => { }); describe('when the raster size is 300 x 200 pixels', () => { beforeEach(() => { - const action: UserSetRasterSize = { type: 'userSetRasterSize', payload: [300, 200] }; + const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] }; store.dispatch(action); }); it('should convert 150,100 in raster space to 0,0 (center) in world space', () => { @@ -55,7 +55,7 @@ describe('rasterToWorld', () => { }); describe('when the user has zoomed to 0.5', () => { beforeEach(() => { - const action: UserScaled = { type: 'userScaled', payload: [0.5, 0.5] }; + const action: CameraAction = { type: 'userScaled', payload: [0.5, 0.5] }; store.dispatch(action); }); it('should convert 150, 100 (center) to 0, 0 (center) in world space', () => { @@ -64,7 +64,7 @@ describe('rasterToWorld', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: UserSetPanningOffset = { type: 'userPanned', payload: [-50, -50] }; + const action: CameraAction = { type: 'userSetPanningOffset', payload: [-50, -50] }; store.dispatch(action); }); it('should convert 100,150 in raster space to 0,0 (center) in world space', () => { @@ -79,13 +79,13 @@ describe('rasterToWorld', () => { }); describe('when the user has panned to the right by 350 and up by 250', () => { beforeEach(() => { - const action: UserSetPanningOffset = { type: 'userPanned', payload: [-350, -250] }; + const action: CameraAction = { type: 'userSetPanningOffset', payload: [-350, -250] }; store.dispatch(action); }); describe('when the user has scaled to 2', () => { // the viewport will only cover half, or 150x100 instead of 300x200 beforeEach(() => { - const action: UserScaled = { type: 'userScaled', payload: [2, 2] }; + const action: CameraAction = { type: 'userScaled', payload: [2, 2] }; store.dispatch(action); }); // we expect the viewport to be diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 1cda199278f0e3..6af82da4db3643 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -6,20 +6,48 @@ import { Vector2, CameraState, CameraStateWhenPanning } from '../../types'; +interface Viewport { + renderWidth: number; + renderHeight: number; + clippingPlaneRight: number; + clippingPlaneTop: number; + clippingPlaneLeft: number; + clippingPlaneBottom: number; +} + +function viewport(state: CameraState): Viewport { + const renderWidth = state.rasterSize[0]; + const renderHeight = state.rasterSize[1]; + const clippingPlaneRight = renderWidth / 2 / state.scaling[0]; + const clippingPlaneTop = renderHeight / 2 / state.scaling[1]; + + return { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft: -clippingPlaneRight, + clippingPlaneBottom: -clippingPlaneTop, + }; +} + /** * https://en.wikipedia.org/wiki/Orthographic_projection * */ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { - const renderWidth = state.rasterSize[0]; - const renderHeight = state.rasterSize[1]; - const clippingPlaneRight = renderWidth / 2 / state.scaling[0]; - const clippingPlaneTop = renderHeight / 2 / state.scaling[1]; - const clippingPlaneLeft = -clippingPlaneRight; - const clippingPlaneBottom = -clippingPlaneTop; + const { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft, + clippingPlaneBottom, + } = viewport(state); return ([worldX, worldY]) => { const [translationX, translationY] = translationIncludingActivePanning(state); + const [xNdc, yNdc] = orthographicProjection( worldX + translationX, worldY + translationY, @@ -39,14 +67,48 @@ function translationIncludingActivePanning(state: CameraState): Vector2 { const panningDeltaX = state.currentPanningOffset[0] - state.panningOrigin[0]; const panningDeltaY = state.currentPanningOffset[1] - state.panningOrigin[1]; return [ - state.translation[0] + panningDeltaX * state.scaling[0], - state.translation[1] + panningDeltaY * state.scaling[1], + state.translation[0] + panningDeltaX / state.scaling[0], + state.translation[1] + panningDeltaY / state.scaling[1], ]; } else { return state.translation; } } +export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { + const { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft, + clippingPlaneBottom, + } = viewport(state); + + return ([rasterX, rasterY]) => { + const [translationX, translationY] = translationIncludingActivePanning(state); + + // raster to ndc + const ndcX = (rasterX / renderWidth) * 2 - 1; + const ndcY = -1 * ((rasterY / renderHeight) * 2 - 1); + + const [panningTranslatedX, panningTranslatedY] = inverseOrthographicProjection( + ndcX, + ndcY, + clippingPlaneTop, + clippingPlaneRight, + clippingPlaneBottom, + clippingPlaneLeft + ); + return [panningTranslatedX - translationX, panningTranslatedY - translationY]; + }; +}; + +export const scale = (state: CameraState): Vector2 => state.scaling; + +export const userIsPanning = (state: CameraState): state is CameraStateWhenPanning => + state.panningOrigin !== null; + /** * Adjust x, y to be bounded, in scale, of a clipping plane defined by top, right, bottom, left * @@ -92,33 +154,3 @@ function inverseOrthographicProjection( return [xPrime, yPrime]; } - -export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { - const renderWidth = state.rasterSize[0]; - const renderHeight = state.rasterSize[1]; - const clippingPlaneRight = renderWidth / 2 / state.scaling[0]; - const clippingPlaneTop = renderHeight / 2 / state.scaling[1]; - const clippingPlaneLeft = -clippingPlaneRight; - const clippingPlaneBottom = -clippingPlaneTop; - - return ([rasterX, rasterY]) => { - // raster to ndc - const ndcX = (rasterX / renderWidth) * 2 - 1; - const ndcY = -1 * ((rasterY / renderHeight) * 2 - 1); - - const [panningTranslatedX, panningTranslatedY] = inverseOrthographicProjection( - ndcX, - ndcY, - clippingPlaneTop, - clippingPlaneRight, - clippingPlaneBottom, - clippingPlaneLeft - ); - return [panningTranslatedX - state.translation[0], panningTranslatedY - state.translation[1]]; - }; -}; - -export const scale = (state: CameraState): Vector2 => state.scaling; - -export const userIsPanning = (state: CameraState): state is CameraStateWhenPanning => - state.panningOrigin !== null; From 661c726212dc1933b95eeae189181895f7505c30 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 10:36:33 -0500 Subject: [PATCH 19/86] seems like pan and zoom almost works --- .../resolver/store/camera/panning.test.ts | 43 +++++++++++++++++++ .../resolver/store/camera/reducer.ts | 12 ++---- .../resolver/store/camera/selectors.ts | 12 +++--- .../public/embeddables/resolver/types.ts | 2 +- 4 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts 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 new file mode 100644 index 00000000000000..3cfbe843ed4de8 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Store, createStore } from 'redux'; +import { cameraReducer } from './reducer'; +import { CameraState, Vector2 } from '../../types'; +import { CameraAction } from './action'; +import { translation } from './selectors'; + +describe('panning interaction', () => { + let store: Store; + let translationShouldBeCloseTo: (expectedTranslation: Vector2) => void; + + beforeEach(() => { + store = createStore(cameraReducer, undefined); + translationShouldBeCloseTo = expectedTranslation => { + const actualTranslation = translation(store.getState()); + expect(expectedTranslation[0]).toBeCloseTo(actualTranslation[0]); + expect(expectedTranslation[1]).toBeCloseTo(actualTranslation[1]); + }; + }); + describe('when the raster size is 300 x 200 pixels', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] }; + store.dispatch(action); + }); + it('should have a translation of 0,0', () => { + translationShouldBeCloseTo([0, 0]); + }); + describe('when the user has started panning', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userStartedPanning', payload: [100, 100] }; + store.dispatch(action); + }); + it('should have a translation of 0,0', () => { + translationShouldBeCloseTo([0, 0]); + }); + }); + }); +}); 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 b35d20e0cd59f8..e7da7185c6d305 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 @@ -5,7 +5,7 @@ */ import { Reducer } from 'redux'; -import { userIsPanning } from './selectors'; +import { userIsPanning, translation } from './selectors'; import { clamp } from '../../lib/math'; import { CameraState, ResolverAction } from '../../types'; @@ -14,7 +14,7 @@ function initialState(): CameraState { return { scaling: [1, 1] as const, rasterSize: [0, 0] as const, - translation: [0, 0] as const, + translationNotCountingCurrentPanning: [0, 0] as const, panningOrigin: null, currentPanningOffset: null, }; @@ -33,7 +33,7 @@ export const cameraReducer: Reducer = ( } else if (action.type === 'userSetPanningOffset') { return { ...state, - translation: action.payload, + translationNotCountingCurrentPanning: action.payload, }; } else if (action.type === 'userStartedPanning') { return { @@ -52,13 +52,9 @@ export const cameraReducer: Reducer = ( } } else if (action.type === 'userStoppedPanning') { if (userIsPanning(state)) { - // TODO, write some vector2 libs plz - const panningDeltaX = state.currentPanningOffset[0] - state.panningOrigin[0]; - const panningDeltaY = state.currentPanningOffset[1] - state.panningOrigin[1]; - return { ...state, - translation: [state.translation[0] + panningDeltaX, state.translation[1] + panningDeltaY], + translationNotCountingCurrentPanning: translation(state), panningOrigin: null, currentPanningOffset: null, }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 6af82da4db3643..2980e5e9cdfb01 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -46,7 +46,7 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => } = viewport(state); return ([worldX, worldY]) => { - const [translationX, translationY] = translationIncludingActivePanning(state); + const [translationX, translationY] = translation(state); const [xNdc, yNdc] = orthographicProjection( worldX + translationX, @@ -62,16 +62,16 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => }; }; -function translationIncludingActivePanning(state: CameraState): Vector2 { +export function translation(state: CameraState): Vector2 { if (userIsPanning(state)) { const panningDeltaX = state.currentPanningOffset[0] - state.panningOrigin[0]; const panningDeltaY = state.currentPanningOffset[1] - state.panningOrigin[1]; return [ - state.translation[0] + panningDeltaX / state.scaling[0], - state.translation[1] + panningDeltaY / state.scaling[1], + state.translationNotCountingCurrentPanning[0] + panningDeltaX / state.scaling[0], + state.translationNotCountingCurrentPanning[1] + panningDeltaY / state.scaling[1], ]; } else { - return state.translation; + return state.translationNotCountingCurrentPanning; } } @@ -86,7 +86,7 @@ export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => } = viewport(state); return ([rasterX, rasterY]) => { - const [translationX, translationY] = translationIncludingActivePanning(state); + const [translationX, translationY] = translation(state); // raster to ndc const ndcX = (rasterX / renderWidth) * 2 - 1; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 7ffcd21f02542a..07838329a00d73 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -12,7 +12,7 @@ export { ResolverAction } from './actions'; export interface CameraState { readonly scaling: Vector2; readonly rasterSize: Vector2; - readonly translation: Vector2; + readonly translationNotCountingCurrentPanning: Vector2; readonly panningOrigin: Vector2 | null; readonly currentPanningOffset: Vector2 | null; } From 27aca818911d7288457edbc1f65cb18669459004 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 11:16:58 -0500 Subject: [PATCH 20/86] add tests for panning --- .../resolver/store/camera/panning.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 3cfbe843ed4de8..ec691b3ea96823 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 @@ -38,6 +38,33 @@ describe('panning interaction', () => { it('should have a translation of 0,0', () => { translationShouldBeCloseTo([0, 0]); }); + describe('when the user continues to pan 50px up and to the right', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userContinuedPanning', payload: [150, 150] }; + store.dispatch(action); + }); + it('should have a translation of 50,50', () => { + translationShouldBeCloseTo([50, 50]); + }); + describe('when the user then stops panning', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userStoppedPanning' }; + store.dispatch(action); + }); + it('should have a translation of 50,50', () => { + translationShouldBeCloseTo([50, 50]); + }); + }); + describe('when the user then cancels panning', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userCanceledPanning' }; + store.dispatch(action); + }); + it('should have a translation of 0,0', () => { + translationShouldBeCloseTo([0, 0]); + }); + }); + }); }); }); }); From e7323cfe7b7ba76d13516c9fc881bb0091d91d3f Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 13:39:18 -0500 Subject: [PATCH 21/86] add some zoom tests --- .../resolver/store/camera/selectors.ts | 18 ++-- .../resolver/store/camera/test_helpers.ts | 17 ++++ .../resolver/store/camera/zooming.test.ts | 82 +++++++++++++++++++ .../public/embeddables/resolver/types.ts | 5 ++ 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 2980e5e9cdfb01..2e25838f8515d5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Vector2, CameraState, CameraStateWhenPanning } from '../../types'; +import { Vector2, CameraState, CameraStateWhenPanning, AABB } from '../../types'; -interface Viewport { +interface RasterCameraProperties { renderWidth: number; renderHeight: number; clippingPlaneRight: number; @@ -15,7 +15,15 @@ interface Viewport { clippingPlaneBottom: number; } -function viewport(state: CameraState): Viewport { +export function viewableBoundingBox(state: CameraState): AABB { + const { renderWidth, renderHeight } = rasterCameraProperties(state); + return { + minimum: rasterToWorld(state)([0, renderHeight]), + maximum: rasterToWorld(state)([renderWidth, 0]), + }; +} + +function rasterCameraProperties(state: CameraState): RasterCameraProperties { const renderWidth = state.rasterSize[0]; const renderHeight = state.rasterSize[1]; const clippingPlaneRight = renderWidth / 2 / state.scaling[0]; @@ -43,7 +51,7 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => clippingPlaneTop, clippingPlaneLeft, clippingPlaneBottom, - } = viewport(state); + } = rasterCameraProperties(state); return ([worldX, worldY]) => { const [translationX, translationY] = translation(state); @@ -83,7 +91,7 @@ export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => clippingPlaneTop, clippingPlaneLeft, clippingPlaneBottom, - } = viewport(state); + } = rasterCameraProperties(state); return ([rasterX, rasterY]) => { const [translationX, translationY] = translation(state); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts new file mode 100644 index 00000000000000..5a867c18778ec7 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Store } from 'redux'; +import { CameraAction } from './action'; +import { CameraState } from '../../types'; + +export function userScaled( + store: Store, + scalingValue: [number, number] +): void { + const action: CameraAction = { type: 'userScaled', payload: scalingValue }; + store.dispatch(action); +} 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 new file mode 100644 index 00000000000000..592172deb4b012 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CameraAction } from './action'; +import { cameraReducer } from './reducer'; +import { createStore, Store } from 'redux'; +import { CameraState, AABB } from '../../types'; +import { viewableBoundingBox, rasterToWorld } from './selectors'; +import { userScaled } from './test_helpers'; + +describe('zooming', () => { + let store: Store; + + const cameraShouldBeBoundBy = (expectedViewableBoundingBox: AABB): [string, () => void] => { + return [ + `the camera view should be bound by an AABB with a minimum point of ${expectedViewableBoundingBox.minimum} and a maximum point of ${expectedViewableBoundingBox.maximum}`, + () => { + const actual = viewableBoundingBox(store.getState()); + expect(actual.minimum[0]).toBeCloseTo(expectedViewableBoundingBox.minimum[0]); + expect(actual.minimum[1]).toBeCloseTo(expectedViewableBoundingBox.minimum[1]); + expect(actual.maximum[0]).toBeCloseTo(expectedViewableBoundingBox.maximum[0]); + expect(actual.maximum[1]).toBeCloseTo(expectedViewableBoundingBox.maximum[1]); + }, + ]; + }; + beforeEach(() => { + store = createStore(cameraReducer, undefined); + }); + describe('when the raster size is 300 x 200 pixels', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] }; + store.dispatch(action); + }); + it( + ...cameraShouldBeBoundBy({ + minimum: [-150, -100], + maximum: [150, 100], + }) + ); + describe('when the user has zoomed in to 2x', () => { + beforeEach(() => { + userScaled(store, [2, 2]); + }); + it( + ...cameraShouldBeBoundBy({ + minimum: [-75, -50], + maximum: [75, 50], + }) + ); + }); + describe('when the user pans right by 100 pixels', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userSetPanningOffset', payload: [-100, 0] }; + store.dispatch(action); + }); + it( + ...cameraShouldBeBoundBy({ + minimum: [-50, -100], + maximum: [250, 100], + }) + ); + it('should be centered on 100, 0', () => { + const worldCenterPoint = rasterToWorld(store.getState())([150, 100]); + expect(worldCenterPoint[0]).toBeCloseTo(100); + expect(worldCenterPoint[1]).toBeCloseTo(0); + }); + describe('when the user zooms to 2x', () => { + beforeEach(() => { + userScaled(store, [2, 2]); + }); + it('should be centered on 100, 0', () => { + const worldCenterPoint = rasterToWorld(store.getState())([150, 100]); + expect(worldCenterPoint[0]).toBeCloseTo(100); + expect(worldCenterPoint[1]).toBeCloseTo(0); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 07838329a00d73..cb0d15d63a6b84 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -23,3 +23,8 @@ export interface CameraStateWhenPanning extends CameraState { } export type Vector2 = readonly [number, number]; + +export interface AABB { + readonly minimum: Vector2; + readonly maximum: Vector2; +} From 6cd3580f77a0288cfa84a8f62a91e0f99c76b00f Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 15:19:15 -0500 Subject: [PATCH 22/86] Added failing (unimplemented) zoom tests --- .../resolver/store/camera/action.ts | 16 ++++++- .../resolver/store/camera/reducer.ts | 8 ++++ .../resolver/store/camera/test_helpers.ts | 14 +++--- .../resolver/store/camera/zooming.test.ts | 45 +++++++++++++++++-- 4 files changed, 74 insertions(+), 9 deletions(-) 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 ab6c15e35ed347..6b2fd2d11af8e2 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 @@ -6,11 +6,18 @@ import { Vector2 } from '../../types'; +// Sets scaling directly. This is not what mouse interactions should use, more like programatic zooming interface UserScaled { readonly type: 'userScaled'; readonly payload: Vector2; } +interface UserZoomed { + readonly type: 'userZoomed'; + // generally pass mouse wheels deltaY (when deltaMode is pixel) divided by -renderHeight + payload: number; +} + interface UserSetRasterSize { readonly type: 'userSetRasterSize'; readonly payload: Vector2; @@ -39,6 +46,11 @@ interface UserCanceledPanning { readonly type: 'userCanceledPanning'; } +interface UserMovedMouseOnPanningCandidate { + readonly type: 'userMovedMouseOnPanningCandidate'; + readonly payload: Vector2; +} + export type CameraAction = | UserScaled | UserSetRasterSize @@ -46,4 +58,6 @@ export type CameraAction = | UserStartedPanning | UserContinuedPanning | UserStoppedPanning - | UserCanceledPanning; + | UserCanceledPanning + | UserMovedMouseOnPanningCandidate + | UserZoomed; 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 e7da7185c6d305..71444ec2076cab 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 @@ -30,6 +30,14 @@ export const cameraReducer: Reducer = ( ...state, scaling: [clamp(deltaX, 0.1, 3), clamp(deltaY, 0.1, 3)], }; + } else if (action.type === 'userZoomed') { + return { + ...state, + scaling: [ + clamp(state.scaling[0] + action.payload, 0.1, 3), + clamp(state.scaling[1] + action.payload, 0.1, 3), + ], + }; } else if (action.type === 'userSetPanningOffset') { return { ...state, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts index 5a867c18778ec7..c906e621873e68 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts @@ -6,12 +6,16 @@ import { Store } from 'redux'; import { CameraAction } from './action'; -import { CameraState } from '../../types'; +import { CameraState, Vector2 } from '../../types'; -export function userScaled( - store: Store, - scalingValue: [number, number] -): void { +type CameraStore = Store; + +export function userScaled(store: CameraStore, scalingValue: [number, number]): void { const action: CameraAction = { type: 'userScaled', payload: scalingValue }; store.dispatch(action); } + +export function expectVectorsToBeClose(first: Vector2, second: Vector2): void { + expect(first[0]).toBeCloseTo(second[0]); + expect(first[1]).toBeCloseTo(second[1]); +} 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 592172deb4b012..f560cc02b630c8 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 @@ -9,7 +9,7 @@ import { cameraReducer } from './reducer'; import { createStore, Store } from 'redux'; import { CameraState, AABB } from '../../types'; import { viewableBoundingBox, rasterToWorld } from './selectors'; -import { userScaled } from './test_helpers'; +import { userScaled, expectVectorsToBeClose } from './test_helpers'; describe('zooming', () => { let store: Store; @@ -40,7 +40,7 @@ describe('zooming', () => { maximum: [150, 100], }) ); - describe('when the user has zoomed in to 2x', () => { + describe('when the user has scaled in to 2x', () => { beforeEach(() => { userScaled(store, [2, 2]); }); @@ -51,6 +51,45 @@ describe('zooming', () => { }) ); }); + describe('when the user zooms in by 1 zoom unit', () => { + beforeEach(() => { + const action: CameraAction = { + type: 'userZoomed', + payload: 1, + }; + store.dispatch(action); + }); + it( + ...cameraShouldBeBoundBy({ + minimum: [-75, -50], + maximum: [75, 50], + }) + ); + }); + it('the raster position 200, 50 should map to the world position 50, 50', () => { + expectVectorsToBeClose(rasterToWorld(store.getState())([200, 50]), [50, 50]); + }); + describe('when the user has moved their mouse to the raster position 200, 50', () => { + beforeEach(() => { + const action: CameraAction = { + type: 'userMovedMouseOnPanningCandidate', + payload: [200, 50], + }; + store.dispatch(action); + }); + describe('when the user zooms in by 0.5 zoom units', () => { + beforeEach(() => { + const action: CameraAction = { + type: 'userZoomed', + payload: 0.5, + }; + store.dispatch(action); + }); + it('the raster position 200, 50 should map to the world position 50, 50', () => { + expectVectorsToBeClose(rasterToWorld(store.getState())([200, 50]), [50, 50]); + }); + }); + }); describe('when the user pans right by 100 pixels', () => { beforeEach(() => { const action: CameraAction = { type: 'userSetPanningOffset', payload: [-100, 0] }; @@ -67,7 +106,7 @@ describe('zooming', () => { expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); }); - describe('when the user zooms to 2x', () => { + describe('when the user scales to 2x', () => { beforeEach(() => { userScaled(store, [2, 2]); }); From f4326ac50e9b7c911900349264b184ef6efab72d Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 16:03:42 -0500 Subject: [PATCH 23/86] zoom works, at least in the reducer --- .../resolver/store/camera/action.ts | 7 +-- .../resolver/store/camera/reducer.ts | 54 ++++++++++++++++--- .../resolver/store/camera/zooming.test.ts | 9 ++-- .../public/embeddables/resolver/types.ts | 1 + 4 files changed, 59 insertions(+), 12 deletions(-) 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 6b2fd2d11af8e2..cfcce45e7e0aff 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 @@ -46,8 +46,9 @@ interface UserCanceledPanning { readonly type: 'userCanceledPanning'; } -interface UserMovedMouseOnPanningCandidate { - readonly type: 'userMovedMouseOnPanningCandidate'; +interface UserFocusedOnWorldCoordinates { + readonly type: 'userFocusedOnWorldCoordinates'; + // client X and Y of mouse event, adjusted for position of resolver on the page readonly payload: Vector2; } @@ -59,5 +60,5 @@ export type CameraAction = | UserContinuedPanning | UserStoppedPanning | UserCanceledPanning - | UserMovedMouseOnPanningCandidate + | UserFocusedOnWorldCoordinates | UserZoomed; 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 71444ec2076cab..52e1ac22048935 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 @@ -5,7 +5,7 @@ */ import { Reducer } from 'redux'; -import { userIsPanning, translation } from './selectors'; +import { userIsPanning, translation, worldToRaster, rasterToWorld } from './selectors'; import { clamp } from '../../lib/math'; import { CameraState, ResolverAction } from '../../types'; @@ -17,6 +17,7 @@ function initialState(): CameraState { translationNotCountingCurrentPanning: [0, 0] as const, panningOrigin: null, currentPanningOffset: null, + latestFocusedWorldCoordinates: null, }; } @@ -31,13 +32,49 @@ export const cameraReducer: Reducer = ( scaling: [clamp(deltaX, 0.1, 3), clamp(deltaY, 0.1, 3)], }; } else if (action.type === 'userZoomed') { - return { + const newScaleX = clamp(state.scaling[0] + action.payload, 0.1, 3); + const newScaleY = clamp(state.scaling[1] + action.payload, 0.1, 3); + console.log( + 'scaleX', + state.scaling[0], + 'scaleY', + state.scaling[1], + 'newScaleX', + newScaleX, + 'newScaleY', + newScaleY + ); + const stateWithNewScaling: CameraState = { ...state, - scaling: [ - clamp(state.scaling[0] + action.payload, 0.1, 3), - clamp(state.scaling[1] + action.payload, 0.1, 3), - ], + scaling: [newScaleX, newScaleY], }; + // TODO, test that asserts that this behavior doesn't happen when user is panning + if (state.latestFocusedWorldCoordinates !== null && userIsPanning(state) === false) { + const rasterOfLastFocusedWorldCoordinates = worldToRaster(state)( + state.latestFocusedWorldCoordinates + ); + const worldCoordinateThereNow = rasterToWorld(stateWithNewScaling)( + rasterOfLastFocusedWorldCoordinates + ); + const delta = [ + worldCoordinateThereNow[0] - state.latestFocusedWorldCoordinates[0], + worldCoordinateThereNow[1] - state.latestFocusedWorldCoordinates[1], + ]; + + return { + ...stateWithNewScaling, + translationNotCountingCurrentPanning: [ + stateWithNewScaling.translationNotCountingCurrentPanning[0] + delta[0], + stateWithNewScaling.translationNotCountingCurrentPanning[1] + delta[1], + ], + }; + + // if no lastMousePosition, we're good + // get world coordinates of lastMousePosition using old state + // get raster coordinates of lastMousue + } else { + return stateWithNewScaling; + } } else if (action.type === 'userSetPanningOffset') { return { ...state, @@ -80,6 +117,11 @@ export const cameraReducer: Reducer = ( ...state, rasterSize: action.payload, }; + } else if (action.type === 'userFocusedOnWorldCoordinates') { + return { + ...state, + latestFocusedWorldCoordinates: action.payload, + }; } 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 f560cc02b630c8..0f840823eea950 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 @@ -8,7 +8,7 @@ import { CameraAction } from './action'; import { cameraReducer } from './reducer'; import { createStore, Store } from 'redux'; import { CameraState, AABB } from '../../types'; -import { viewableBoundingBox, rasterToWorld } from './selectors'; +import { viewableBoundingBox, rasterToWorld, worldToRaster } from './selectors'; import { userScaled, expectVectorsToBeClose } from './test_helpers'; describe('zooming', () => { @@ -72,11 +72,14 @@ describe('zooming', () => { describe('when the user has moved their mouse to the raster position 200, 50', () => { beforeEach(() => { const action: CameraAction = { - type: 'userMovedMouseOnPanningCandidate', - payload: [200, 50], + type: 'userFocusedOnWorldCoordinates', + payload: rasterToWorld(store.getState())([200, 50]), }; store.dispatch(action); }); + it('should have focused the world position 50, 50', () => { + expectVectorsToBeClose(store.getState().latestFocusedWorldCoordinates, [50, 50]); + }); describe('when the user zooms in by 0.5 zoom units', () => { beforeEach(() => { const action: CameraAction = { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index cb0d15d63a6b84..094ff1ba9994f9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -15,6 +15,7 @@ export interface CameraState { readonly translationNotCountingCurrentPanning: Vector2; readonly panningOrigin: Vector2 | null; readonly currentPanningOffset: Vector2 | null; + readonly latestFocusedWorldCoordinates: Vector2 | null; } export interface CameraStateWhenPanning extends CameraState { From 9736f6bc96d54b618ca37655b433e5f5c4ff6f74 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 16:29:58 -0500 Subject: [PATCH 24/86] Panning and zooming is almost done --- .../resolver/store/camera/reducer.ts | 15 +--- .../resolver/store/camera/zooming.test.ts | 2 +- .../embeddables/resolver/view/index.tsx | 85 ++++++++++++++----- 3 files changed, 67 insertions(+), 35 deletions(-) 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 52e1ac22048935..f6324620822283 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 @@ -34,16 +34,6 @@ export const cameraReducer: Reducer = ( } else if (action.type === 'userZoomed') { const newScaleX = clamp(state.scaling[0] + action.payload, 0.1, 3); const newScaleY = clamp(state.scaling[1] + action.payload, 0.1, 3); - console.log( - 'scaleX', - state.scaling[0], - 'scaleY', - state.scaling[1], - 'newScaleX', - newScaleX, - 'newScaleY', - newScaleY - ); const stateWithNewScaling: CameraState = { ...state, scaling: [newScaleX, newScaleY], @@ -68,10 +58,6 @@ export const cameraReducer: Reducer = ( stateWithNewScaling.translationNotCountingCurrentPanning[1] + delta[1], ], }; - - // if no lastMousePosition, we're good - // get world coordinates of lastMousePosition using old state - // get raster coordinates of lastMousue } else { return stateWithNewScaling; } @@ -87,6 +73,7 @@ export const cameraReducer: Reducer = ( currentPanningOffset: action.payload, }; } else if (action.type === 'userContinuedPanning') { + // TODO make these offsets be in world coordinates as well if (userIsPanning(state)) { 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 0f840823eea950..f36824424908af 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 @@ -8,7 +8,7 @@ import { CameraAction } from './action'; import { cameraReducer } from './reducer'; import { createStore, Store } from 'redux'; import { CameraState, AABB } from '../../types'; -import { viewableBoundingBox, rasterToWorld, worldToRaster } from './selectors'; +import { viewableBoundingBox, rasterToWorld } from './selectors'; import { userScaled, expectVectorsToBeClose } from './test_helpers'; describe('zooming', () => { 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 7dca41e649b1f9..011dce516b532a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -11,7 +11,6 @@ import { Dispatch } from 'redux'; import styled from 'styled-components'; import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; -import { clamp } from '../lib/math'; export const AppRoot: React.FC<{ store: Store; @@ -27,13 +26,27 @@ const useResolverDispatch = () => useDispatch>(); const Diagnostic = styled( React.memo(({ className }: { className?: string }) => { - const scale = useSelector(selectors.scale); const userIsPanning = useSelector(selectors.userIsPanning); const [elementBoundingClientRect, clientRectCallbackFunction] = useAutoUpdatingClientRect(); const dispatch = useResolverDispatch(); + const rasterToWorld = useSelector(selectors.rasterToWorld); + + const worldPositionFromClientPosition = useCallback( + (clientPosition: Vector2): Vector2 | null => { + if (elementBoundingClientRect === undefined) { + return null; + } + return rasterToWorld([ + clientPosition[0] - elementBoundingClientRect.x, + clientPosition[1] - elementBoundingClientRect.y, + ]); + }, + [rasterToWorld, elementBoundingClientRect] + ); + useEffect(() => { if (elementBoundingClientRect !== undefined) { dispatch({ @@ -61,8 +74,19 @@ const Diagnostic = styled( 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, + }); + } }, - [dispatch, userIsPanning] + [dispatch, userIsPanning, worldPositionFromClientPosition] ); const handleMouseUp = useCallback(() => { @@ -75,19 +99,37 @@ const Diagnostic = styled( const handleWheel = useCallback( (event: React.WheelEvent) => { - if (event.ctrlKey) { - /* - * Clamp amount at ±10 percent per action. - * Determining the scale of the deltaY is difficult due to differences in UA. - */ - const scaleDelta = clamp(event.deltaY, -0.1, 0.1); + // we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height + if ( + elementBoundingClientRect !== undefined && + event.ctrlKey && + event.deltaY !== 0 && + event.deltaMode === 0 + ) { dispatch({ - type: 'userScaled', - payload: [scale[0] + scaleDelta, scale[1] + scaleDelta], + type: 'userZoomed', + payload: event.deltaY / elementBoundingClientRect.height, }); } }, - [scale, dispatch] + [elementBoundingClientRect, dispatch] + ); + + // TODO, handle mouse up when no longer on element or event window. ty + + const dotPositions = useMemo( + (): ReadonlyArray => [ + [0, 0], + [0, 100], + [100, 100], + [100, 0], + [100, -100], + [0, -100], + [-100, -100], + [-100, 0], + [-100, 100], + ], + [] ); return ( @@ -99,7 +141,9 @@ const Diagnostic = styled( onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} > - [0, 0], [])} /> + {dotPositions.map(worldPosition => ( + + ))}
); }) @@ -139,20 +183,21 @@ const DiagnosticDot = styled( const worldToRaster = useSelector(selectors.worldToRaster); const [left, top] = worldToRaster(worldPosition); const style = { - left: (left - 30).toString() + 'px', - top: (top - 30).toString() + 'px', + left: (left - 20).toString() + 'px', + top: (top - 20).toString() + 'px', }; return ( - - 🤤 + + ({worldPosition[0]}, {worldPosition[1]}) ); }) )` position: absolute; - width: 60px; - height: 60px; + width: 40px; + height: 40px; text-align: center; - font-size: 60px; + font-size: 20px; user-select: none; + border: 1px solid black; `; From b314ddb8df45937152a7a8dd38418c19859aa13f Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 4 Dec 2019 17:11:26 -0500 Subject: [PATCH 25/86] style diagnostic dots --- .../public/embeddables/resolver/view/index.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 011dce516b532a..3817adeeef32da 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -141,8 +141,8 @@ const Diagnostic = styled( onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} > - {dotPositions.map(worldPosition => ( - + {dotPositions.map((worldPosition, index) => ( + ))}
); @@ -188,7 +188,9 @@ const DiagnosticDot = styled( }; return ( - ({worldPosition[0]}, {worldPosition[1]}) + x: {worldPosition[0]} +
+ y: {worldPosition[1]}
); }) @@ -196,8 +198,12 @@ const DiagnosticDot = styled( position: absolute; width: 40px; height: 40px; - text-align: center; - font-size: 20px; + 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; `; From 0fb78a2cd9217f5fae4c974ca0bf82d549a43cba Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 5 Dec 2019 11:39:43 -0500 Subject: [PATCH 26/86] refactor stuff to use vectors and matrices --- .../embeddables/resolver/lib/matrix3.test.ts | 21 ++++ .../embeddables/resolver/lib/matrix3.ts | 50 ++++++++++ .../resolver/lib/transformation.test.ts | 14 +++ .../resolver/lib/transformation.ts | 40 ++++++++ .../embeddables/resolver/lib/vector2.ts | 23 +++++ .../resolver/store/camera/selectors.ts | 96 ++++++++++--------- .../public/embeddables/resolver/types.ts | 14 +++ 7 files changed, 214 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.test.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.test.ts new file mode 100644 index 00000000000000..c0e9f982e23acf --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { multiply } from './matrix3'; +describe('matrix3', () => { + it('can multiply two matrix3s', () => { + expect(multiply([1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18])).toEqual([ + 84, + 90, + 96, + 201, + 216, + 231, + 318, + 342, + 366, + ]); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts new file mode 100644 index 00000000000000..430065f5fe4a8d --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Matrix3 } from '../types'; + +export function multiply( + [a11, a12, a13, a21, a22, a23, a31, a32, a33]: Matrix3, + [b11, b12, b13, b21, b22, b23, b31, b32, b33]: Matrix3 +): Matrix3 { + const s11 = a11 * b11 + a12 * b21 + a13 * b31; + const s12 = a11 * b12 + a12 * b22 + a13 * b32; + const s13 = a11 * b13 + a12 * b23 + a13 * b33; + + const s21 = a21 * b11 + a22 * b21 + a23 * b31; + const s22 = a21 * b12 + a22 * b22 + a23 * b32; + const s23 = a21 * b13 + a22 * b23 + a23 * b33; + + const s31 = a31 * b11 + a32 * b21 + a33 * b31; + const s32 = a31 * b12 + a32 * b22 + a33 * b32; + const s33 = a31 * b13 + a32 * b23 + a33 * b33; + + // prettier-ignore + return [ + s11, s12, s13, + s21, s22, s23, + s31, s32, s33, + ]; +} + +export function add( + [a11, a12, a13, a21, a22, a23, a31, a32, a33]: Matrix3, + [b11, b12, b13, b21, b22, b23, b31, b32, b33]: Matrix3 +): Matrix3 { + return [ + a11 + b11, + a12 + b12, + a13 + b13, + + a21 + b21, + a22 + b22, + a23 + b23, + + a31 + b31, + a32 + b32, + a33 + b33, + ]; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts new file mode 100644 index 00000000000000..cfe4f8593ab0f9 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { applyMatrix3 } from './vector2'; +import { scalingTransformation } from './transformation'; + +describe('transforms', () => { + it('applying a scale matrix to a vector2 can invert the y value', () => { + expect(applyMatrix3([1, 2], scalingTransformation([1, -1, 1]))).toEqual([1, -2]); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts new file mode 100644 index 00000000000000..6d8c0b5a155f58 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Matrix3, Vector3, Vector2 } from '../types'; + +export function inverseOrthographicProjection( + top: number, + right: number, + bottom: number, + left: number +): Matrix3 { + const m11 = (right - left) / 2; + const m41 = (right + left) / (right - left); + + const m22 = (top - bottom) / 2; + const m42 = (top + bottom) / (top - bottom); + + return [m11, 0, m41, 0, m22, m42, 0, 0, 0]; +} + +export function scalingTransformation([x, y, z]: Vector3): Matrix3 { + // prettier-ignore + return [ + x, 0, 0, + 0, y, 0, + 0, 0, z + ] +} + +export function translationTransformation([x, y]: Vector2): Matrix3 { + // prettier-ignore + return [ + 1, 0, x, + 0, 1, y, + 0, 0, 0 + ] +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts new file mode 100644 index 00000000000000..0cd11f235d1f72 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Vector2, Matrix3 } from '../types'; + +// TODO, should I call these sum, product, difference, etc instead of add, multiply, subtract? we use that term for other stuff like dot product and cross product (where these is no common verb version) +export function add(a: Vector2, b: Vector2): Vector2 { + return [a[0] + b[0], a[1] + b[1]]; +} + +export function subtract(a: Vector2, b: Vector2): Vector2 { + return [a[0] - b[0], a[1] - b[1]]; +} + +export function divide(a: Vector2, b: Vector2): Vector2 { + return [a[0] / b[0], a[1] / b[1]]; +} + +export function applyMatrix3([x, y]: Vector2, [m11, m12, m13, m21, m22, m23]: Matrix3): Vector2 { + return [x * m11 + y * m12 + m13, y * m21 + y * m22 + m23]; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 2e25838f8515d5..056bfa21b27b46 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -5,6 +5,13 @@ */ import { Vector2, CameraState, CameraStateWhenPanning, AABB } from '../../types'; +import { subtract, divide, add, applyMatrix3 } from '../../lib/vector2'; +import { multiply, add as addMatrix } from '../../lib/matrix3'; +import { + inverseOrthographicProjection, + scalingTransformation, + translationTransformation, +} from '../../lib/transformation'; interface RasterCameraProperties { renderWidth: number; @@ -72,18 +79,18 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => export function translation(state: CameraState): Vector2 { if (userIsPanning(state)) { - const panningDeltaX = state.currentPanningOffset[0] - state.panningOrigin[0]; - const panningDeltaY = state.currentPanningOffset[1] - state.panningOrigin[1]; - return [ - state.translationNotCountingCurrentPanning[0] + panningDeltaX / state.scaling[0], - state.translationNotCountingCurrentPanning[1] + panningDeltaY / state.scaling[1], - ]; + return add( + state.translationNotCountingCurrentPanning, + divide(subtract(state.currentPanningOffset, state.panningOrigin), state.scaling) + ); } else { return state.translationNotCountingCurrentPanning; } } -export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { +export const rasterToWorld: ( + state: CameraState +) => (rasterPosition: Vector2) => Vector2 = state => { const { renderWidth, renderHeight, @@ -92,23 +99,44 @@ export const rasterToWorld: (state: CameraState) => (worldPosition: Vector2) => clippingPlaneLeft, clippingPlaneBottom, } = rasterCameraProperties(state); - - return ([rasterX, rasterY]) => { - const [translationX, translationY] = translation(state); - - // raster to ndc - const ndcX = (rasterX / renderWidth) * 2 - 1; - const ndcY = -1 * ((rasterY / renderHeight) * 2 - 1); - - const [panningTranslatedX, panningTranslatedY] = inverseOrthographicProjection( - ndcX, - ndcY, - clippingPlaneTop, - clippingPlaneRight, - clippingPlaneBottom, - clippingPlaneLeft - ); - return [panningTranslatedX - translationX, panningTranslatedY - translationY]; + const [translationX, translationY] = translation(state); + + // Translate for the 'camera' + // prettier-ignore + const translationMatrix = [ + 0, 0, -translationX, + 0, 0, -translationY, + 0, 0, 0 + ] as const; + + // TODO, define screen, raster, and world coordinates + + // Convert a vector in screen space to NDC space + const screenToNDC = multiply( + scalingTransformation([1, -1, 1]), + // prettier-ignore + [ + 2 / renderWidth, 0, -1, + 2 / renderHeight, 0, -1, + 0, 0, 0 + ] as const + ); + + const projection = addMatrix( + multiply( + inverseOrthographicProjection( + clippingPlaneTop, + clippingPlaneRight, + clippingPlaneBottom, + clippingPlaneLeft + ), + screenToNDC + ), + translationMatrix + ); + + return rasterPosition => { + return applyMatrix3(rasterPosition, projection); }; }; @@ -142,23 +170,3 @@ function orthographicProjection( return [xPrime, yPrime]; } - -function inverseOrthographicProjection( - x: number, - y: number, - top: number, - right: number, - bottom: number, - left: number -): [number, number] { - const m11 = (right - left) / 2; - const m41 = (right + left) / (right - left); - - const m22 = (top - bottom) / 2; - const m42 = (top + bottom) / (top - bottom); - - const xPrime = x * m11 + m41; - const yPrime = y * m22 + m42; - - return [xPrime, yPrime]; -} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 094ff1ba9994f9..c5e05551d44814 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -25,7 +25,21 @@ export interface CameraStateWhenPanning extends CameraState { export type Vector2 = readonly [number, number]; +export type Vector3 = readonly [number, number, number]; + export interface AABB { readonly minimum: Vector2; readonly maximum: Vector2; } + +export type Matrix3 = readonly [ + number, + number, + number, + number, + number, + number, + number, + number, + number +]; From 1fa6edba235253086fc46db7e508e0579799a5dc Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 5 Dec 2019 12:36:40 -0500 Subject: [PATCH 27/86] more matrices --- .../resolver/lib/transformation.ts | 35 ++++-- .../resolver/store/camera/selectors.ts | 111 ++++++++---------- 2 files changed, 78 insertions(+), 68 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts index 6d8c0b5a155f58..b28e9c97c4dbf2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Matrix3, Vector3, Vector2 } from '../types'; +import { Matrix3, Vector2 } from '../types'; export function inverseOrthographicProjection( top: number, @@ -13,20 +13,41 @@ export function inverseOrthographicProjection( left: number ): Matrix3 { const m11 = (right - left) / 2; - const m41 = (right + left) / (right - left); + const m13 = (right + left) / (right - left); const m22 = (top - bottom) / 2; - const m42 = (top + bottom) / (top - bottom); + const m23 = (top + bottom) / (top - bottom); - return [m11, 0, m41, 0, m22, m42, 0, 0, 0]; + return [m11, 0, m13, 0, m22, m23, 0, 0, 0]; } -export function scalingTransformation([x, y, z]: Vector3): Matrix3 { +/** + * Adjust x, y to be bounded, in scale, of a clipping plane defined by top, right, bottom, left + * + * See explanation: + * https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix + */ +export function orthographicProjection( + top: number, + right: number, + bottom: number, + left: number +): Matrix3 { + const m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds + const m13 = -((right + left) / (right - left)); + + const m22 = 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds + const m23 = -((top + bottom) / (top - bottom)); + + return [m11, 0, m13, 0, m22, m23, 0, 0, 0]; +} + +export function scalingTransformation([x, y]: Vector2): Matrix3 { // prettier-ignore return [ x, 0, 0, 0, y, 0, - 0, 0, z + 0, 0, 0 ] } @@ -35,6 +56,6 @@ export function translationTransformation([x, y]: Vector2): Matrix3 { return [ 1, 0, x, 0, 1, y, - 0, 0, 0 + 0, 0, 1 ] } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 056bfa21b27b46..89132f805fb2a7 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -10,6 +10,7 @@ import { multiply, add as addMatrix } from '../../lib/matrix3'; import { inverseOrthographicProjection, scalingTransformation, + orthographicProjection, translationTransformation, } from '../../lib/transformation'; @@ -60,20 +61,33 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => clippingPlaneBottom, } = rasterCameraProperties(state); - return ([worldX, worldY]) => { - const [translationX, translationY] = translation(state); - - const [xNdc, yNdc] = orthographicProjection( - worldX + translationX, - worldY + translationY, - clippingPlaneTop, - clippingPlaneRight, - clippingPlaneBottom, - clippingPlaneLeft - ); + const projection = multiply( + // 5. convert from 0->2 to 0->rasterWidth (or height) + scalingTransformation([renderWidth / 2, renderHeight / 2]), + addMatrix( + // 4. add one to change range from -1->1 to 0->2 + [0, 0, 1, 0, 0, 1, 0, 0, 0], + multiply( + // 3. invert y since CSS has inverted y + scalingTransformation([1, -1]), + multiply( + // 2. scale to clipping plane + orthographicProjection( + clippingPlaneTop, + clippingPlaneRight, + clippingPlaneBottom, + clippingPlaneLeft + ), + // 1. adjust for camera + translationTransformation(translation(state)) + ) + ) + ) + ); - // ndc to raster - return [(renderWidth * (xNdc + 1)) / 2, (renderHeight * (-yNdc + 1)) / 2]; + // TODO this no longer needs to be a function, just a matrix now + return worldPosition => { + return applyMatrix3(worldPosition, projection); }; }; @@ -99,42 +113,43 @@ export const rasterToWorld: ( clippingPlaneLeft, clippingPlaneBottom, } = rasterCameraProperties(state); - const [translationX, translationY] = translation(state); - - // Translate for the 'camera' - // prettier-ignore - const translationMatrix = [ - 0, 0, -translationX, - 0, 0, -translationY, - 0, 0, 0 - ] as const; // TODO, define screen, raster, and world coordinates - // Convert a vector in screen space to NDC space - const screenToNDC = multiply( - scalingTransformation([1, -1, 1]), - // prettier-ignore - [ - 2 / renderWidth, 0, -1, - 2 / renderHeight, 0, -1, - 0, 0, 0 - ] as const - ); + const [translationX, translationY] = translation(state); const projection = addMatrix( + // 4. Translate for the 'camera' + // prettier-ignore + [ + 0, 0, -translationX, + 0, 0, -translationY, + 0, 0, 0 + ] as const, multiply( + // 3. make values in range of clipping planes inverseOrthographicProjection( clippingPlaneTop, clippingPlaneRight, clippingPlaneBottom, clippingPlaneLeft ), - screenToNDC - ), - translationMatrix + multiply( + // 2 Invert Y since CSS has inverted y + scalingTransformation([1, -1]), + // 1. convert screen coordinates to NDC + // e.g. for x-axis, divide by renderWidth then multiply by 2 and subtract by one so the value is in range of -1->1 + // prettier-ignore + [ + 2 / renderWidth, 0, -1, + 2 / renderHeight, 0, -1, + 0, 0, 0 + ] as const + ) + ) ); + // TODO, this no longer needs to be a function, can just be a matrix return rasterPosition => { return applyMatrix3(rasterPosition, projection); }; @@ -144,29 +159,3 @@ export const scale = (state: CameraState): Vector2 => state.scaling; export const userIsPanning = (state: CameraState): state is CameraStateWhenPanning => state.panningOrigin !== null; - -/** - * Adjust x, y to be bounded, in scale, of a clipping plane defined by top, right, bottom, left - * - * See explanation: - * https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix - */ -function orthographicProjection( - x: number, - y: number, - top: number, - right: number, - bottom: number, - left: number -): [number, number] { - const m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds - const m41 = -((right + left) / (right - left)); - - const m22 = 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds - const m42 = -((top + bottom) / (top - bottom)); - - const xPrime = x * m11 + m41; - const yPrime = y * m22 + m42; - - return [xPrime, yPrime]; -} From 5157b2d6aa24bdfd3fc32857e59fe43be1440c32 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 5 Dec 2019 16:33:14 -0500 Subject: [PATCH 28/86] brent made me do it --- .../resolver/lib/transformation.test.ts | 2 +- .../resolver/store/camera/reducer.ts | 2 ++ .../resolver/store/camera/selectors.ts | 8 ++--- .../resolver/store/camera/zooming.test.ts | 5 ++- .../public/embeddables/resolver/types.ts | 33 +++++++++++++------ .../embeddables/resolver/view/index.tsx | 19 ++++++++--- 6 files changed, 48 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts index cfe4f8593ab0f9..3fe6941279bc53 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts @@ -9,6 +9,6 @@ import { scalingTransformation } from './transformation'; describe('transforms', () => { it('applying a scale matrix to a vector2 can invert the y value', () => { - expect(applyMatrix3([1, 2], scalingTransformation([1, -1, 1]))).toEqual([1, -2]); + expect(applyMatrix3([1, 2], scalingTransformation([1, -1]))).toEqual([1, -2]); }); }); 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 f6324620822283..8b93eed9f0cebf 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 @@ -76,7 +76,9 @@ export const cameraReducer: Reducer = ( // TODO make these offsets be in world coordinates as well if (userIsPanning(state)) { return { + // This logic means, if the user calls `userContinuedPanning` without starting panning first, we start automatically basically? ...state, + panningOrigin: state.panningOrigin === null ? action.payload : state.panningOrigin, currentPanningOffset: action.payload, }; } else { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 89132f805fb2a7..cde1f649572a49 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Vector2, CameraState, CameraStateWhenPanning, AABB } from '../../types'; +import { Vector2, CameraState, AABB } from '../../types'; import { subtract, divide, add, applyMatrix3 } from '../../lib/vector2'; import { multiply, add as addMatrix } from '../../lib/matrix3'; import { @@ -92,7 +92,7 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => }; export function translation(state: CameraState): Vector2 { - if (userIsPanning(state)) { + if (state.currentPanningOffset !== null && state.panningOrigin !== null) { return add( state.translationNotCountingCurrentPanning, divide(subtract(state.currentPanningOffset, state.panningOrigin), state.scaling) @@ -102,6 +102,7 @@ export function translation(state: CameraState): Vector2 { } } +// TODO, can we generically invert this matrix from worldToRaster? export const rasterToWorld: ( state: CameraState ) => (rasterPosition: Vector2) => Vector2 = state => { @@ -157,5 +158,4 @@ export const rasterToWorld: ( export const scale = (state: CameraState): Vector2 => state.scaling; -export const userIsPanning = (state: CameraState): state is CameraStateWhenPanning => - state.panningOrigin !== null; +export const userIsPanning = (state: CameraState): boolean => state.panningOrigin !== null; 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 f36824424908af..9d00fa642e435c 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 @@ -78,7 +78,10 @@ describe('zooming', () => { store.dispatch(action); }); it('should have focused the world position 50, 50', () => { - expectVectorsToBeClose(store.getState().latestFocusedWorldCoordinates, [50, 50]); + const coords = store.getState().latestFocusedWorldCoordinates; + expect(coords).not.toBeNull(); + // TODO, revisit this + expectVectorsToBeClose(coords!, [50, 50]); }); describe('when the user zooms in by 0.5 zoom units', () => { beforeEach(() => { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index c5e05551d44814..e0087776d31acc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -4,24 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ export interface ResolverState { - camera: CameraState; + readonly camera: CameraState; } export { ResolverAction } from './actions'; -export interface CameraState { +type PanningState = + | { + // When we start panning, track the clientX and Y + readonly panningOrigin: Vector2; + + // This value holds the current clientX and Y when panning. The difference between this + // and panningOrigin is a vector that expresses how much and where we've panned this time + readonly currentPanningOffset: Vector2; + } + | { + readonly panningOrigin: null; + readonly currentPanningOffset: null; + }; + +export type CameraState = PanningState & { readonly scaling: Vector2; + /** + * the size (in pixels) of the REsolver element + */ readonly rasterSize: Vector2; + // When we finish panning, we add the current panning vector to this vector to get the position of the camera. + // When we start panning again, we add the 'currentPanningOffset - panningOrigin' to this value to get the position of the camera readonly translationNotCountingCurrentPanning: Vector2; - readonly panningOrigin: Vector2 | null; - readonly currentPanningOffset: Vector2 | null; + // This is the world coordinates of the current mouse position. used to keep wheel zoom smooth (any other stuff eventually?) readonly latestFocusedWorldCoordinates: Vector2 | null; -} - -export interface CameraStateWhenPanning extends CameraState { - readonly panningOrigin: Vector2; - readonly currentPanningOffset: Vector2; -} +}; export type Vector2 = readonly [number, number]; 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 3817adeeef32da..8b4095a15e0b82 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -12,9 +12,7 @@ import styled from 'styled-components'; import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; -export const AppRoot: React.FC<{ - store: Store; -}> = React.memo(({ store }) => { +export const AppRoot = React.memo(({ store }: { store: Store }) => { return ( @@ -60,6 +58,7 @@ const Diagnostic = styled( (event: React.MouseEvent) => { dispatch({ type: 'userStartedPanning', + // TODO why is this negative? payload: [event.clientX, -event.clientY], }); }, @@ -71,6 +70,7 @@ const Diagnostic = styled( if (event.buttons === 1 && userIsPanning) { dispatch({ type: 'userContinuedPanning', + // TODO why is this negative? payload: [event.clientX, -event.clientY], }); } @@ -148,22 +148,31 @@ const Diagnostic = styled( ); }) )` + /* TODO, this is not a concern of Resolver. its parent needs to do this probably? */ display: flex; flex-grow: 1; position: relative; `; -function useAutoUpdatingClientRect() { +/** + * 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. + * Updates automatically when the window resizes. TODO: better Englishe here + */ +function useAutoUpdatingClientRect(): [DOMRect | undefined, (node: Element | null) => void] { const [rect, setRect] = useState(); const nodeRef = useRef(); const ref = useCallback((node: Element | null) => { + // why do we have to deal /w both null and undefined? TODO nodeRef.current = node === null ? undefined : node; if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); + // TODO, this isn't really a concern of Resolver. + // The parent should inform resolver that it needs to rerender useEffect(() => { window.addEventListener('resize', handler, { passive: true }); return () => { @@ -175,7 +184,7 @@ function useAutoUpdatingClientRect() { } } }, []); - return [rect, ref] as const; + return [rect, ref]; } const DiagnosticDot = styled( From 73b08da8d76677792ed9e85a4455719cbbed4c34 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 5 Dec 2019 16:50:09 -0500 Subject: [PATCH 29/86] mousmove is on window now --- .../public/embeddables/resolver/view/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 8b4095a15e0b82..74b3198395da12 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -66,7 +66,7 @@ const Diagnostic = styled( ); const handleMouseMove = useCallback( - (event: React.MouseEvent) => { + (event: MouseEvent) => { if (event.buttons === 1 && userIsPanning) { dispatch({ type: 'userContinuedPanning', @@ -115,6 +115,13 @@ const Diagnostic = styled( [elementBoundingClientRect, dispatch] ); + useEffect(() => { + window.addEventListener('mousemove', handleMouseMove, { passive: true }); + return () => { + window.removeEventListener('mousemove', handleMouseMove); + }; + }, [handleMouseMove]); + // TODO, handle mouse up when no longer on element or event window. ty const dotPositions = useMemo( @@ -136,7 +143,6 @@ const Diagnostic = styled(
Date: Thu, 5 Dec 2019 16:57:46 -0500 Subject: [PATCH 30/86] move mouseup event to window. blacklist userFocusedOnWorldCoordinates --- .../public/embeddables/resolver/store/camera/action.ts | 1 + .../endpoint/public/embeddables/resolver/store/index.ts | 6 +++++- .../endpoint/public/embeddables/resolver/view/index.tsx | 8 +++++++- 3 files changed, 13 insertions(+), 2 deletions(-) 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 cfcce45e7e0aff..5cab1ee1718942 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 @@ -46,6 +46,7 @@ interface UserCanceledPanning { readonly type: 'userCanceledPanning'; } +// This action is blacklisted in redux dev tools interface UserFocusedOnWorldCoordinates { readonly type: 'userFocusedOnWorldCoordinates'; // client X and Y of mouse event, adjusted for position of resolver on the page 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 89ebeb15d90535..7a435b8b1e3bb4 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -10,7 +10,10 @@ import { HttpServiceBase } from '../../../../../../../src/core/public'; export const storeFactory = ({ httpServiceBase }: { httpServiceBase: HttpServiceBase }) => { interface SomethingThatMightHaveReduxDevTools { - __REDUX_DEVTOOLS_EXTENSION__?: (options?: { name?: string }) => StoreEnhancer; + __REDUX_DEVTOOLS_EXTENSION__?: (options?: { + name?: string; + actionsBlacklist: readonly string[]; + }) => StoreEnhancer; } const windowWhichMightHaveReduxDevTools = window as SomethingThatMightHaveReduxDevTools; const store = createStore( @@ -18,6 +21,7 @@ export const storeFactory = ({ httpServiceBase }: { httpServiceBase: HttpService windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__ && windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__({ name: 'Resolver', + actionsBlacklist: ['userFocusedOnWorldCoordinates'], }) ); 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 74b3198395da12..dd82c5681c88aa 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -115,6 +115,13 @@ const Diagnostic = styled( [elementBoundingClientRect, dispatch] ); + useEffect(() => { + window.addEventListener('mouseup', handleMouseUp, { passive: true }); + return () => { + window.removeEventListener('mousemove', handleMouseUp); + }; + }, [handleMouseUp]); + useEffect(() => { window.addEventListener('mousemove', handleMouseMove, { passive: true }); return () => { @@ -145,7 +152,6 @@ const Diagnostic = styled( ref={clientRectCallbackFunction} onWheel={handleWheel} onMouseDown={handleMouseDown} - onMouseUp={handleMouseUp} > {dotPositions.map((worldPosition, index) => ( From d75315831fef45927509b95427d75ab29823fa15 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 6 Dec 2019 09:22:57 -0500 Subject: [PATCH 31/86] fix mouseup handler --- .../plugins/endpoint/public/embeddables/resolver/view/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dd82c5681c88aa..c10d925ceaf649 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -118,7 +118,7 @@ const Diagnostic = styled( useEffect(() => { window.addEventListener('mouseup', handleMouseUp, { passive: true }); return () => { - window.removeEventListener('mousemove', handleMouseUp); + window.removeEventListener('mouseup', handleMouseUp); }; }, [handleMouseUp]); From 183528a5e37104db9d7b7db83bc36db464bd86ff Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 6 Dec 2019 15:08:35 -0500 Subject: [PATCH 32/86] prevent default when handling wheel --- .../embeddables/resolver/view/index.tsx | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) 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 c10d925ceaf649..cdd92bea7d2814 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -24,11 +24,13 @@ const useResolverDispatch = () => useDispatch>(); const Diagnostic = styled( React.memo(({ className }: { className?: string }) => { - const userIsPanning = useSelector(selectors.userIsPanning); + const dispatch = useResolverDispatch(); - const [elementBoundingClientRect, clientRectCallbackFunction] = useAutoUpdatingClientRect(); + const [ref, setRef] = useState(null); - const dispatch = useResolverDispatch(); + const userIsPanning = useSelector(selectors.userIsPanning); + + const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect(); const rasterToWorld = useSelector(selectors.rasterToWorld); @@ -98,7 +100,7 @@ const Diagnostic = styled( }, [dispatch, userIsPanning]); const handleWheel = useCallback( - (event: React.WheelEvent) => { + (event: WheelEvent) => { // we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height if ( elementBoundingClientRect !== undefined && @@ -106,6 +108,7 @@ const Diagnostic = styled( event.deltaY !== 0 && event.deltaMode === 0 ) { + event.preventDefault(); dispatch({ type: 'userZoomed', payload: event.deltaY / elementBoundingClientRect.height, @@ -146,13 +149,27 @@ const Diagnostic = styled( [] ); + const refCallback = useCallback( + (node: null | HTMLDivElement) => { + setRef(node); + clientRectCallback(node); + }, + [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]); + return ( -
+
{dotPositions.map((worldPosition, index) => ( ))} From 1dc668380d6e50086e1a35b1bdf39267bfaf0c0f Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 6 Dec 2019 19:30:20 -0500 Subject: [PATCH 33/86] invert wheel zoom --- .../endpoint/public/embeddables/resolver/view/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 cdd92bea7d2814..0a47a0d58f71c5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -54,7 +54,7 @@ const Diagnostic = styled( payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], }); } - }, [elementBoundingClientRect, dispatch]); + }, [dispatch, elementBoundingClientRect]); const handleMouseDown = useCallback( (event: React.MouseEvent) => { @@ -111,7 +111,7 @@ const Diagnostic = styled( event.preventDefault(); dispatch({ type: 'userZoomed', - payload: event.deltaY / elementBoundingClientRect.height, + payload: (-2 * event.deltaY) / elementBoundingClientRect.height, }); } }, From e007b383ca06e8e82483aeac6d50c3ff8db47b04 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 17 Dec 2019 11:38:37 -0500 Subject: [PATCH 34/86] add comments to types and stuff --- .../public/embeddables/resolver/lib/math.ts | 3 +++ .../public/embeddables/resolver/lib/matrix3.ts | 6 ++++++ .../embeddables/resolver/lib/transformation.ts | 14 +++++++++++++- .../public/embeddables/resolver/lib/vector2.ts | 13 ++++++++++++- .../endpoint/public/embeddables/resolver/types.ts | 8 +++++++- 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts index 08c39a5baa6a07..c59db31c39e827 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * Return `value` unless it is less than `minimum`, in which case return `minimum` or unless it is greater than `maximum`, in which case return `maximum`. + */ export function clamp(value: number, minimum: number, maximum: number) { return Math.max(Math.min(value, maximum), minimum); } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts index 430065f5fe4a8d..0b4a72b9d79a6f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts @@ -6,6 +6,9 @@ import { Matrix3 } from '../types'; +/** + * Return a new matrix which is the product of the first and second matrix. + */ export function multiply( [a11, a12, a13, a21, a22, a23, a31, a32, a33]: Matrix3, [b11, b12, b13, b21, b22, b23, b31, b32, b33]: Matrix3 @@ -30,6 +33,9 @@ export function multiply( ]; } +/** + * Return a new matrix which is the sum of the two passed in. + */ export function add( [a11, a12, a13, a21, a22, a23, a31, a32, a33]: Matrix3, [b11, b12, b13, b21, b22, b23, b31, b32, b33]: Matrix3 diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts index b28e9c97c4dbf2..3084ce0eacdb4c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts @@ -6,6 +6,9 @@ import { Matrix3, Vector2 } from '../types'; +/** + * The inverse of `orthographicProjection`. + */ export function inverseOrthographicProjection( top: number, right: number, @@ -22,10 +25,11 @@ export function inverseOrthographicProjection( } /** - * Adjust x, y to be bounded, in scale, of a clipping plane defined by top, right, bottom, left + * Adjust x, y to be bounded, in scale, of a clipping plane defined by top, right, bottom, left. * * See explanation: * https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix + * https://en.wikipedia.org/wiki/Orthographic_projection */ export function orthographicProjection( top: number, @@ -42,6 +46,10 @@ export function orthographicProjection( return [m11, 0, m13, 0, m22, m23, 0, 0, 0]; } +/** + * Returns a 2D transformation matrix that when applied to a vector will scale the vector by `x` and `y` in their respective axises. + * See https://en.wikipedia.org/wiki/Scaling_(geometry)#Matrix_representation + */ export function scalingTransformation([x, y]: Vector2): Matrix3 { // prettier-ignore return [ @@ -51,6 +59,10 @@ export function scalingTransformation([x, y]: Vector2): Matrix3 { ] } +/** + * Returns a 2D transformation matrix that when applied to a vector will translate by `x` and `y` in their respective axises. + * See https://en.wikipedia.org/wiki/Translation_(geometry)#Matrix_representation + */ export function translationTransformation([x, y]: Vector2): Matrix3 { // prettier-ignore return [ diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts index 0cd11f235d1f72..b39672682bcda7 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts @@ -5,19 +5,30 @@ */ import { Vector2, Matrix3 } from '../types'; -// TODO, should I call these sum, product, difference, etc instead of add, multiply, subtract? we use that term for other stuff like dot product and cross product (where these is no common verb version) +/** + * Returns a vector which is the sum of `a` and `b`. + */ export function add(a: Vector2, b: Vector2): Vector2 { return [a[0] + b[0], a[1] + b[1]]; } +/** + * Returns a vector which is the difference of `a` and `b`. + */ export function subtract(a: Vector2, b: Vector2): Vector2 { return [a[0] - b[0], a[1] - b[1]]; } +/** + * Returns a vector which is the quotient of `a` and `b`. + */ export function divide(a: Vector2, b: Vector2): Vector2 { return [a[0] / b[0], a[1] / b[1]]; } +/** + * Returns a vector which is the result of applying a 2D transformation matrix to the provided vector. + */ export function applyMatrix3([x, y]: Vector2, [m11, m12, m13, m21, m22, m23]: Matrix3): Vector2 { return [x * m11 + y * m12 + m13, y * m21 + y * m22 + m23]; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index e0087776d31acc..979a59641352f3 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -34,17 +34,23 @@ export type CameraState = PanningState & { readonly translationNotCountingCurrentPanning: Vector2; // This is the world coordinates of the current mouse position. used to keep wheel zoom smooth (any other stuff eventually?) readonly latestFocusedWorldCoordinates: Vector2 | null; -}; +} export type Vector2 = readonly [number, number]; export type Vector3 = readonly [number, number, number]; +/** + * A rectangle with sides that align with the `x` and `y` axises. + */ export interface AABB { readonly minimum: Vector2; readonly maximum: Vector2; } +/** + * A 2D transformation matrix in row-major order + */ export type Matrix3 = readonly [ number, number, From 59703e614d2f1b33c8e0696d0cdfe279c115836c Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 17 Dec 2019 14:18:12 -0500 Subject: [PATCH 35/86] change camera state shape. rename projectionMatrix --- .../resolver/store/camera/reducer.ts | 28 ++++++++++--------- .../resolver/store/camera/selectors.ts | 19 ++++--------- .../store/camera/world_to_raster.test.ts | 7 +++-- .../embeddables/resolver/store/selectors.ts | 5 +++- .../public/embeddables/resolver/types.ts | 23 +++++++-------- .../embeddables/resolver/view/index.tsx | 5 ++-- 6 files changed, 42 insertions(+), 45 deletions(-) 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 8b93eed9f0cebf..13f4f8bd177f74 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 @@ -5,7 +5,8 @@ */ import { Reducer } from 'redux'; -import { userIsPanning, translation, worldToRaster, rasterToWorld } from './selectors'; +import { applyMatrix3 } from '../../lib/vector2'; +import { userIsPanning, translation, projectionMatrix, rasterToWorld } from './selectors'; import { clamp } from '../../lib/math'; import { CameraState, ResolverAction } from '../../types'; @@ -15,8 +16,6 @@ function initialState(): CameraState { scaling: [1, 1] as const, rasterSize: [0, 0] as const, translationNotCountingCurrentPanning: [0, 0] as const, - panningOrigin: null, - currentPanningOffset: null, latestFocusedWorldCoordinates: null, }; } @@ -40,8 +39,9 @@ export const cameraReducer: Reducer = ( }; // TODO, test that asserts that this behavior doesn't happen when user is panning if (state.latestFocusedWorldCoordinates !== null && userIsPanning(state) === false) { - const rasterOfLastFocusedWorldCoordinates = worldToRaster(state)( - state.latestFocusedWorldCoordinates + const rasterOfLastFocusedWorldCoordinates = applyMatrix3( + state.latestFocusedWorldCoordinates, + projectionMatrix(state) ); const worldCoordinateThereNow = rasterToWorld(stateWithNewScaling)( rasterOfLastFocusedWorldCoordinates @@ -69,8 +69,10 @@ export const cameraReducer: Reducer = ( } else if (action.type === 'userStartedPanning') { return { ...state, - panningOrigin: action.payload, - currentPanningOffset: action.payload, + panning: { + origin: action.payload, + currentOffset: action.payload, + }, }; } else if (action.type === 'userContinuedPanning') { // TODO make these offsets be in world coordinates as well @@ -78,8 +80,10 @@ export const cameraReducer: Reducer = ( return { // This logic means, if the user calls `userContinuedPanning` without starting panning first, we start automatically basically? ...state, - panningOrigin: state.panningOrigin === null ? action.payload : state.panningOrigin, - currentPanningOffset: action.payload, + panning: { + origin: state.panning ? state.panning.origin : action.payload, + currentOffset: action.payload, + }, }; } else { return state; @@ -89,8 +93,7 @@ export const cameraReducer: Reducer = ( return { ...state, translationNotCountingCurrentPanning: translation(state), - panningOrigin: null, - currentPanningOffset: null, + panning: undefined, }; } else { return state; @@ -98,8 +101,7 @@ export const cameraReducer: Reducer = ( } else if (action.type === 'userCanceledPanning') { return { ...state, - panningOrigin: null, - currentPanningOffset: null, + panning: undefined, }; } else if (action.type === 'userSetRasterSize') { return { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index cde1f649572a49..7c5b29c1208dc6 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Vector2, CameraState, AABB } from '../../types'; +import { Vector2, CameraState, AABB, Matrix3 } from '../../types'; import { subtract, divide, add, applyMatrix3 } from '../../lib/vector2'; import { multiply, add as addMatrix } from '../../lib/matrix3'; import { @@ -49,9 +49,8 @@ function rasterCameraProperties(state: CameraState): RasterCameraProperties { /** * https://en.wikipedia.org/wiki/Orthographic_projection - * */ -export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => Vector2 = state => { +export const projectionMatrix: (state: CameraState) => Matrix3 = state => { const { renderWidth, renderHeight, @@ -61,7 +60,7 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => clippingPlaneBottom, } = rasterCameraProperties(state); - const projection = multiply( + return multiply( // 5. convert from 0->2 to 0->rasterWidth (or height) scalingTransformation([renderWidth / 2, renderHeight / 2]), addMatrix( @@ -84,25 +83,19 @@ export const worldToRaster: (state: CameraState) => (worldPosition: Vector2) => ) ) ); - - // TODO this no longer needs to be a function, just a matrix now - return worldPosition => { - return applyMatrix3(worldPosition, projection); - }; }; export function translation(state: CameraState): Vector2 { - if (state.currentPanningOffset !== null && state.panningOrigin !== null) { + if (state.panning) { return add( state.translationNotCountingCurrentPanning, - divide(subtract(state.currentPanningOffset, state.panningOrigin), state.scaling) + divide(subtract(state.panning.currentOffset, state.panning.origin), state.scaling) ); } else { return state.translationNotCountingCurrentPanning; } } -// TODO, can we generically invert this matrix from worldToRaster? export const rasterToWorld: ( state: CameraState ) => (rasterPosition: Vector2) => Vector2 = state => { @@ -158,4 +151,4 @@ export const rasterToWorld: ( export const scale = (state: CameraState): Vector2 => state.scaling; -export const userIsPanning = (state: CameraState): boolean => state.panningOrigin !== null; +export const userIsPanning = (state: CameraState): boolean => state.panning !== undefined; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts index ad3da4dc89773d..8849abcf427ac0 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts @@ -8,15 +8,16 @@ import { Store, createStore } from 'redux'; import { CameraAction } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; -import { worldToRaster } from './selectors'; +import { projectionMatrix } from './selectors'; +import { applyMatrix3 } from '../../lib/vector2'; -describe('worldToRaster', () => { +describe('projectionMatrix', () => { let store: Store; let compare: (worldPosition: [number, number], expectedRasterPosition: [number, number]) => void; beforeEach(() => { store = createStore(cameraReducer, undefined); compare = (worldPosition: [number, number], expectedRasterPosition: [number, number]) => { - const [rasterX, rasterY] = worldToRaster(store.getState())(worldPosition); + const [rasterX, rasterY] = applyMatrix3(worldPosition, projectionMatrix(store.getState())); expect(rasterX).toBeCloseTo(expectedRasterPosition[0]); expect(rasterY).toBeCloseTo(expectedRasterPosition[1]); }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index c78092570c5bb2..c8ef1ec13776ef 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -7,7 +7,10 @@ import * as cameraSelectors from './camera/selectors'; import { ResolverState } from '../types'; -export const worldToRaster = composeSelectors(cameraStateSelector, cameraSelectors.worldToRaster); +export const projectionMatrix = composeSelectors( + cameraStateSelector, + cameraSelectors.projectionMatrix +); export const rasterToWorld = composeSelectors(cameraStateSelector, cameraSelectors.rasterToWorld); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 979a59641352f3..568edaf1fef5e8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -3,27 +3,24 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +/** + * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. + */ export interface ResolverState { readonly camera: CameraState; } export { ResolverAction } from './actions'; -type PanningState = - | { - // When we start panning, track the clientX and Y - readonly panningOrigin: Vector2; +interface PanningState { + readonly origin: Vector2; + readonly currentOffset: Vector2; +} - // This value holds the current clientX and Y when panning. The difference between this - // and panningOrigin is a vector that expresses how much and where we've panned this time - readonly currentPanningOffset: Vector2; - } - | { - readonly panningOrigin: null; - readonly currentPanningOffset: null; - }; +export interface CameraState { + readonly panning?: PanningState; -export type CameraState = PanningState & { readonly scaling: Vector2; /** * the size (in pixels) of the REsolver element 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 0a47a0d58f71c5..fffc31c7da3ac2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -9,6 +9,7 @@ import { Store } from 'redux'; import { Provider, useSelector, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import styled from 'styled-components'; +import { applyMatrix3 } from '../lib/vector2'; import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; @@ -218,8 +219,8 @@ function useAutoUpdatingClientRect(): [DOMRect | undefined, (node: Element | nul const DiagnosticDot = styled( React.memo(({ className, worldPosition }: { className?: string; worldPosition: Vector2 }) => { - const worldToRaster = useSelector(selectors.worldToRaster); - const [left, top] = worldToRaster(worldPosition); + const projectionMatrix = useSelector(selectors.projectionMatrix); + const [left, top] = applyMatrix3(worldPosition, projectionMatrix); const style = { left: (left - 20).toString() + 'px', top: (top - 20).toString() + 'px', From 567331259a8ad68d2a77154fa23cddc9151f7fc5 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 17 Dec 2019 14:46:29 -0500 Subject: [PATCH 36/86] rename projection matrix test file. also raster to world is now a matrix --- ...aster.test.ts => projection_matrix.test.ts} | 0 .../store/camera/raster_to_world.test.ts | 3 ++- .../resolver/store/camera/reducer.ts | 5 ++--- .../resolver/store/camera/selectors.ts | 18 +++++------------- .../resolver/store/camera/zooming.test.ts | 14 +++++++++----- .../public/embeddables/resolver/view/index.tsx | 11 +++++++---- 6 files changed, 25 insertions(+), 26 deletions(-) rename x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/{world_to_raster.test.ts => projection_matrix.test.ts} (100%) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts similarity index 100% rename from x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/world_to_raster.test.ts rename to x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts index d0dd649efa95d5..e641e10ce301dd 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts @@ -9,6 +9,7 @@ import { CameraAction } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; import { rasterToWorld } from './selectors'; +import { applyMatrix3 } from '../../lib/vector2'; describe('rasterToWorld', () => { let store: Store; @@ -16,7 +17,7 @@ describe('rasterToWorld', () => { beforeEach(() => { store = createStore(cameraReducer, undefined); compare = (rasterPosition: [number, number], expectedWorldPosition: [number, number]) => { - const [worldX, worldY] = rasterToWorld(store.getState())(rasterPosition); + const [worldX, worldY] = applyMatrix3(rasterPosition, rasterToWorld(store.getState())); expect(worldX).toBeCloseTo(expectedWorldPosition[0]); expect(worldY).toBeCloseTo(expectedWorldPosition[1]); }; 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 13f4f8bd177f74..2ebb01bd00b916 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 @@ -43,9 +43,8 @@ export const cameraReducer: Reducer = ( state.latestFocusedWorldCoordinates, projectionMatrix(state) ); - const worldCoordinateThereNow = rasterToWorld(stateWithNewScaling)( - rasterOfLastFocusedWorldCoordinates - ); + const matrix = rasterToWorld(stateWithNewScaling); + const worldCoordinateThereNow = applyMatrix3(rasterOfLastFocusedWorldCoordinates, matrix); const delta = [ worldCoordinateThereNow[0] - state.latestFocusedWorldCoordinates[0], worldCoordinateThereNow[1] - state.latestFocusedWorldCoordinates[1], diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 7c5b29c1208dc6..bec82c5750897b 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -25,9 +25,10 @@ interface RasterCameraProperties { export function viewableBoundingBox(state: CameraState): AABB { const { renderWidth, renderHeight } = rasterCameraProperties(state); + const matrix = rasterToWorld(state); return { - minimum: rasterToWorld(state)([0, renderHeight]), - maximum: rasterToWorld(state)([renderWidth, 0]), + minimum: applyMatrix3([0, renderHeight], matrix), + maximum: applyMatrix3([renderWidth, 0], matrix), }; } @@ -96,9 +97,7 @@ export function translation(state: CameraState): Vector2 { } } -export const rasterToWorld: ( - state: CameraState -) => (rasterPosition: Vector2) => Vector2 = state => { +export const rasterToWorld: (state: CameraState) => Matrix3 = state => { const { renderWidth, renderHeight, @@ -108,11 +107,9 @@ export const rasterToWorld: ( clippingPlaneBottom, } = rasterCameraProperties(state); - // TODO, define screen, raster, and world coordinates - const [translationX, translationY] = translation(state); - const projection = addMatrix( + return addMatrix( // 4. Translate for the 'camera' // prettier-ignore [ @@ -142,11 +139,6 @@ export const rasterToWorld: ( ) ) ); - - // TODO, this no longer needs to be a function, can just be a matrix - return rasterPosition => { - return applyMatrix3(rasterPosition, projection); - }; }; export const scale = (state: CameraState): Vector2 => state.scaling; 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 9d00fa642e435c..f6db23ca952954 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 @@ -10,6 +10,7 @@ import { createStore, Store } from 'redux'; import { CameraState, AABB } from '../../types'; import { viewableBoundingBox, rasterToWorld } from './selectors'; import { userScaled, expectVectorsToBeClose } from './test_helpers'; +import { applyMatrix3 } from '../../lib/vector2'; describe('zooming', () => { let store: Store; @@ -67,13 +68,13 @@ describe('zooming', () => { ); }); it('the raster position 200, 50 should map to the world position 50, 50', () => { - expectVectorsToBeClose(rasterToWorld(store.getState())([200, 50]), [50, 50]); + expectVectorsToBeClose(applyMatrix3([200, 50], rasterToWorld(store.getState())), [50, 50]); }); describe('when the user has moved their mouse to the raster position 200, 50', () => { beforeEach(() => { const action: CameraAction = { type: 'userFocusedOnWorldCoordinates', - payload: rasterToWorld(store.getState())([200, 50]), + payload: applyMatrix3([200, 50], rasterToWorld(store.getState())), }; store.dispatch(action); }); @@ -92,7 +93,10 @@ describe('zooming', () => { store.dispatch(action); }); it('the raster position 200, 50 should map to the world position 50, 50', () => { - expectVectorsToBeClose(rasterToWorld(store.getState())([200, 50]), [50, 50]); + expectVectorsToBeClose(applyMatrix3([200, 50], rasterToWorld(store.getState())), [ + 50, + 50, + ]); }); }); }); @@ -108,7 +112,7 @@ describe('zooming', () => { }) ); it('should be centered on 100, 0', () => { - const worldCenterPoint = rasterToWorld(store.getState())([150, 100]); + const worldCenterPoint = applyMatrix3([150, 100], rasterToWorld(store.getState())); expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); }); @@ -117,7 +121,7 @@ describe('zooming', () => { userScaled(store, [2, 2]); }); it('should be centered on 100, 0', () => { - const worldCenterPoint = rasterToWorld(store.getState())([150, 100]); + const worldCenterPoint = applyMatrix3([150, 100], rasterToWorld(store.getState())); expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); }); 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 fffc31c7da3ac2..5406a29e5a668f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -40,10 +40,13 @@ const Diagnostic = styled( if (elementBoundingClientRect === undefined) { return null; } - return rasterToWorld([ - clientPosition[0] - elementBoundingClientRect.x, - clientPosition[1] - elementBoundingClientRect.y, - ]); + return applyMatrix3( + [ + clientPosition[0] - elementBoundingClientRect.x, + clientPosition[1] - elementBoundingClientRect.y, + ], + rasterToWorld + ); }, [rasterToWorld, elementBoundingClientRect] ); From 750369cfa50f96f11f927f78c4c1e06561ef9ff8 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 17 Dec 2019 14:49:14 -0500 Subject: [PATCH 37/86] rename inverse projection matrix --- .../store/camera/raster_to_world.test.ts | 9 ++++--- .../resolver/store/camera/reducer.ts | 4 +-- .../resolver/store/camera/selectors.ts | 4 +-- .../resolver/store/camera/zooming.test.ts | 27 ++++++++++++------- .../embeddables/resolver/store/selectors.ts | 5 +++- .../embeddables/resolver/view/index.tsx | 6 ++--- 6 files changed, 35 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts index e641e10ce301dd..9afb3f84882094 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts @@ -8,16 +8,19 @@ import { Store, createStore } from 'redux'; import { CameraAction } from './action'; import { CameraState } from '../../types'; import { cameraReducer } from './reducer'; -import { rasterToWorld } from './selectors'; +import { inverseProjectionMatrix } from './selectors'; import { applyMatrix3 } from '../../lib/vector2'; -describe('rasterToWorld', () => { +describe('inverseProjectionMatrix', () => { let store: Store; let compare: (worldPosition: [number, number], expectedRasterPosition: [number, number]) => void; beforeEach(() => { store = createStore(cameraReducer, undefined); compare = (rasterPosition: [number, number], expectedWorldPosition: [number, number]) => { - const [worldX, worldY] = applyMatrix3(rasterPosition, rasterToWorld(store.getState())); + const [worldX, worldY] = applyMatrix3( + rasterPosition, + inverseProjectionMatrix(store.getState()) + ); expect(worldX).toBeCloseTo(expectedWorldPosition[0]); expect(worldY).toBeCloseTo(expectedWorldPosition[1]); }; 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 2ebb01bd00b916..df5a63372d02ab 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 @@ -6,7 +6,7 @@ import { Reducer } from 'redux'; import { applyMatrix3 } from '../../lib/vector2'; -import { userIsPanning, translation, projectionMatrix, rasterToWorld } from './selectors'; +import { userIsPanning, translation, projectionMatrix, inverseProjectionMatrix } from './selectors'; import { clamp } from '../../lib/math'; import { CameraState, ResolverAction } from '../../types'; @@ -43,7 +43,7 @@ export const cameraReducer: Reducer = ( state.latestFocusedWorldCoordinates, projectionMatrix(state) ); - const matrix = rasterToWorld(stateWithNewScaling); + const matrix = inverseProjectionMatrix(stateWithNewScaling); const worldCoordinateThereNow = applyMatrix3(rasterOfLastFocusedWorldCoordinates, matrix); const delta = [ worldCoordinateThereNow[0] - state.latestFocusedWorldCoordinates[0], diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index bec82c5750897b..e9229bfa4af214 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -25,7 +25,7 @@ interface RasterCameraProperties { export function viewableBoundingBox(state: CameraState): AABB { const { renderWidth, renderHeight } = rasterCameraProperties(state); - const matrix = rasterToWorld(state); + const matrix = inverseProjectionMatrix(state); return { minimum: applyMatrix3([0, renderHeight], matrix), maximum: applyMatrix3([renderWidth, 0], matrix), @@ -97,7 +97,7 @@ export function translation(state: CameraState): Vector2 { } } -export const rasterToWorld: (state: CameraState) => Matrix3 = state => { +export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => { const { renderWidth, renderHeight, 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 f6db23ca952954..bdcd6dfd07c39f 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 @@ -8,7 +8,7 @@ import { CameraAction } from './action'; import { cameraReducer } from './reducer'; import { createStore, Store } from 'redux'; import { CameraState, AABB } from '../../types'; -import { viewableBoundingBox, rasterToWorld } from './selectors'; +import { viewableBoundingBox, inverseProjectionMatrix } from './selectors'; import { userScaled, expectVectorsToBeClose } from './test_helpers'; import { applyMatrix3 } from '../../lib/vector2'; @@ -68,13 +68,16 @@ describe('zooming', () => { ); }); it('the raster position 200, 50 should map to the world position 50, 50', () => { - expectVectorsToBeClose(applyMatrix3([200, 50], rasterToWorld(store.getState())), [50, 50]); + expectVectorsToBeClose(applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), [ + 50, + 50, + ]); }); describe('when the user has moved their mouse to the raster position 200, 50', () => { beforeEach(() => { const action: CameraAction = { type: 'userFocusedOnWorldCoordinates', - payload: applyMatrix3([200, 50], rasterToWorld(store.getState())), + payload: applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), }; store.dispatch(action); }); @@ -93,10 +96,10 @@ describe('zooming', () => { store.dispatch(action); }); it('the raster position 200, 50 should map to the world position 50, 50', () => { - expectVectorsToBeClose(applyMatrix3([200, 50], rasterToWorld(store.getState())), [ - 50, - 50, - ]); + expectVectorsToBeClose( + applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), + [50, 50] + ); }); }); }); @@ -112,7 +115,10 @@ describe('zooming', () => { }) ); it('should be centered on 100, 0', () => { - const worldCenterPoint = applyMatrix3([150, 100], rasterToWorld(store.getState())); + const worldCenterPoint = applyMatrix3( + [150, 100], + inverseProjectionMatrix(store.getState()) + ); expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); }); @@ -121,7 +127,10 @@ describe('zooming', () => { userScaled(store, [2, 2]); }); it('should be centered on 100, 0', () => { - const worldCenterPoint = applyMatrix3([150, 100], rasterToWorld(store.getState())); + const worldCenterPoint = applyMatrix3( + [150, 100], + inverseProjectionMatrix(store.getState()) + ); expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index c8ef1ec13776ef..734aaf0041984c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -12,7 +12,10 @@ export const projectionMatrix = composeSelectors( cameraSelectors.projectionMatrix ); -export const rasterToWorld = composeSelectors(cameraStateSelector, cameraSelectors.rasterToWorld); +export const inverseProjectionMatrix = composeSelectors( + cameraStateSelector, + cameraSelectors.inverseProjectionMatrix +); export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale); 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 5406a29e5a668f..15a7b027a24057 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -33,7 +33,7 @@ const Diagnostic = styled( const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect(); - const rasterToWorld = useSelector(selectors.rasterToWorld); + const inverseProjectionMatrix = useSelector(selectors.inverseProjectionMatrix); const worldPositionFromClientPosition = useCallback( (clientPosition: Vector2): Vector2 | null => { @@ -45,10 +45,10 @@ const Diagnostic = styled( clientPosition[0] - elementBoundingClientRect.x, clientPosition[1] - elementBoundingClientRect.y, ], - rasterToWorld + inverseProjectionMatrix ); }, - [rasterToWorld, elementBoundingClientRect] + [inverseProjectionMatrix, elementBoundingClientRect] ); useEffect(() => { From b074f7b7d8de5b67ada0d689a3089d9257bf5469 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 17 Dec 2019 14:50:13 -0500 Subject: [PATCH 38/86] rename inverse projection matrix test file --- ...{raster_to_world.test.ts => inverse_projection_matrix.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/{raster_to_world.test.ts => inverse_projection_matrix.test.ts} (100%) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts similarity index 100% rename from x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/raster_to_world.test.ts rename to x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts From daddda494fcae349ea3a353c7b7ec15cbb7b30fc Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 17 Dec 2019 15:53:35 -0500 Subject: [PATCH 39/86] panning actions take screen coordinates --- .../resolver/store/camera/panning.test.ts | 2 +- .../resolver/store/camera/selectors.ts | 16 ++++++++++------ .../public/embeddables/resolver/view/index.tsx | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) 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 ec691b3ea96823..d7d6a01d64b2d3 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, 150] }; + const action: CameraAction = { type: 'userContinuedPanning', 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/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index e9229bfa4af214..3f6300f4b989c7 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -14,7 +14,7 @@ import { translationTransformation, } from '../../lib/transformation'; -interface RasterCameraProperties { +interface ClippingPlane { renderWidth: number; renderHeight: number; clippingPlaneRight: number; @@ -24,7 +24,7 @@ interface RasterCameraProperties { } export function viewableBoundingBox(state: CameraState): AABB { - const { renderWidth, renderHeight } = rasterCameraProperties(state); + const { renderWidth, renderHeight } = clippingPlane(state); const matrix = inverseProjectionMatrix(state); return { minimum: applyMatrix3([0, renderHeight], matrix), @@ -32,7 +32,7 @@ export function viewableBoundingBox(state: CameraState): AABB { }; } -function rasterCameraProperties(state: CameraState): RasterCameraProperties { +function clippingPlane(state: CameraState): ClippingPlane { const renderWidth = state.rasterSize[0]; const renderHeight = state.rasterSize[1]; const clippingPlaneRight = renderWidth / 2 / state.scaling[0]; @@ -59,7 +59,7 @@ export const projectionMatrix: (state: CameraState) => Matrix3 = state => { clippingPlaneTop, clippingPlaneLeft, clippingPlaneBottom, - } = rasterCameraProperties(state); + } = clippingPlane(state); return multiply( // 5. convert from 0->2 to 0->rasterWidth (or height) @@ -90,7 +90,11 @@ export function translation(state: CameraState): Vector2 { if (state.panning) { return add( state.translationNotCountingCurrentPanning, - divide(subtract(state.panning.currentOffset, state.panning.origin), state.scaling) + divide(subtract(state.panning.currentOffset, state.panning.origin), [ + state.scaling[0], + // Invert `y` since the `.panning` vectors are in screen coordinates and therefore have backwards `y` + -state.scaling[1], + ]) ); } else { return state.translationNotCountingCurrentPanning; @@ -105,7 +109,7 @@ export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => clippingPlaneTop, clippingPlaneLeft, clippingPlaneBottom, - } = rasterCameraProperties(state); + } = clippingPlane(state); const [translationX, translationY] = translation(state); 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 15a7b027a24057..881dac75d51f4e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -65,7 +65,7 @@ const Diagnostic = styled( dispatch({ type: 'userStartedPanning', // TODO why is this negative? - payload: [event.clientX, -event.clientY], + payload: [event.clientX, event.clientY], }); }, [dispatch] @@ -77,7 +77,7 @@ const Diagnostic = styled( dispatch({ type: 'userContinuedPanning', // TODO why is this negative? - payload: [event.clientX, -event.clientY], + payload: [event.clientX, event.clientY], }); } // TODO, don't fire two actions here. make userContinuedPanning also pass world position From bc5d15fd0038d3eb697169f55b96d940067399e2 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 19 Dec 2019 12:47:26 -0500 Subject: [PATCH 40/86] document stuff --- .../resolver/store/camera/action.ts | 11 ++++- .../resolver/store/camera/reducer.ts | 1 - .../public/embeddables/resolver/types.ts | 42 ++++++++++++++++--- .../embeddables/resolver/view/index.tsx | 2 - 4 files changed, 46 insertions(+), 10 deletions(-) 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 5cab1ee1718942..b934677a2e86e0 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 @@ -6,20 +6,27 @@ import { Vector2 } from '../../types'; -// Sets scaling directly. This is not what mouse interactions should use, more like programatic zooming interface UserScaled { readonly type: 'userScaled'; + /** + * A vector who's `x` and `y` component will be the new scaling factors for the projection. + */ readonly payload: Vector2; } interface UserZoomed { readonly type: 'userZoomed'; - // generally pass mouse wheels deltaY (when deltaMode is pixel) divided by -renderHeight + /** + * A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`, pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels. + */ payload: number; } interface UserSetRasterSize { readonly type: 'userSetRasterSize'; + /** + * The dimensions of the Resolver component in pixels. The Resolver component should not be scrollable itself. + */ readonly payload: Vector2; } 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 df5a63372d02ab..467ba0efea5195 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 @@ -74,7 +74,6 @@ export const cameraReducer: Reducer = ( }, }; } else if (action.type === 'userContinuedPanning') { - // TODO make these offsets be in world coordinates as well if (userIsPanning(state)) { return { // This logic means, if the user calls `userContinuedPanning` without starting panning first, we start automatically basically? diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 568edaf1fef5e8..70ad7be17764e7 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -8,28 +8,54 @@ * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. */ export interface ResolverState { + /** + * Contains the state of the camera. This includes panning interactions, transform, and projection. + */ readonly camera: CameraState; } export { ResolverAction } from './actions'; interface PanningState { + /** + * Screen coordinate vector representing the starting point when panning. + */ readonly origin: Vector2; + + /** + * Screen coordinate vector representing the current point when panning. + */ readonly currentOffset: Vector2; } +/** + * Redux state for the virtual 'camera' used by Resolver. + */ export interface CameraState { + /** + * Contains the starting and current position of the pointer when the user is panning the map. + */ readonly panning?: PanningState; + /** + * Scales the coordinate system, used for zooming. + */ readonly scaling: Vector2; + /** - * the size (in pixels) of the REsolver element + * The size (in pixels) of the Resolver component. */ readonly rasterSize: Vector2; - // When we finish panning, we add the current panning vector to this vector to get the position of the camera. - // When we start panning again, we add the 'currentPanningOffset - panningOrigin' to this value to get the position of the camera + + /** + * The camera world transform not counting any change from panning. When panning finishes, this value is updated to account for it. + * Use the `transform` selector to get the transform adjusted for panning. + */ readonly translationNotCountingCurrentPanning: Vector2; - // This is the world coordinates of the current mouse position. used to keep wheel zoom smooth (any other stuff eventually?) + + /** + * The world coordinates that the pointing device was last over. This is used during mousewheel zoom. + */ readonly latestFocusedWorldCoordinates: Vector2 | null; } @@ -41,12 +67,18 @@ export type Vector3 = readonly [number, number, number]; * A rectangle with sides that align with the `x` and `y` axises. */ export interface AABB { + /** + * Vector who's `x` component is the _left_ side of the AABB and who's `y` component is the _bottom_ side of the AABB. + **/ readonly minimum: Vector2; + /** + * Vector who's `x` component is the _right_ side of the AABB and who's `y` component is the _bottom_ side of the AABB. + **/ readonly maximum: Vector2; } /** - * A 2D transformation matrix in row-major order + * A 2D transformation matrix in row-major order. */ export type Matrix3 = readonly [ number, 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 881dac75d51f4e..c5e40b27ff1489 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -64,7 +64,6 @@ const Diagnostic = styled( (event: React.MouseEvent) => { dispatch({ type: 'userStartedPanning', - // TODO why is this negative? payload: [event.clientX, event.clientY], }); }, @@ -76,7 +75,6 @@ const Diagnostic = styled( if (event.buttons === 1 && userIsPanning) { dispatch({ type: 'userContinuedPanning', - // TODO why is this negative? payload: [event.clientX, event.clientY], }); } From 4669d7712035e91707fc444c2b8060f1b83a958f Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 19 Dec 2019 13:09:47 -0500 Subject: [PATCH 41/86] Documenting more types --- .../resolver/store/camera/action.ts | 19 ++++++++++++++++++- .../public/embeddables/resolver/types.ts | 4 ++-- .../embeddables/resolver/view/index.tsx | 5 +---- 3 files changed, 21 insertions(+), 7 deletions(-) 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 b934677a2e86e0..3251e624484bf9 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 @@ -30,18 +30,32 @@ interface UserSetRasterSize { readonly payload: Vector2; } +// TODO, fix and rename this. or remove it +// this is used to directly set the transform of the camera, +// it should be named something like 'userSetPositionOfCamera'. It should work in conjunction with panning (it doesn't, or at least its not tested or tried.) interface UserSetPanningOffset { readonly type: 'userSetPanningOffset'; + /** + * The world transform of the camera + */ readonly payload: Vector2; } 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.) + * 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; } @@ -56,7 +70,10 @@ interface UserCanceledPanning { // This action is blacklisted in redux dev tools interface UserFocusedOnWorldCoordinates { readonly type: 'userFocusedOnWorldCoordinates'; - // client X and Y of mouse event, adjusted for position of resolver on the page + /** + * 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 + */ readonly payload: Vector2; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 70ad7be17764e7..225a592f3c9827 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export { ResolverAction } from './actions'; + /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. */ @@ -14,8 +16,6 @@ export interface ResolverState { readonly camera: CameraState; } -export { ResolverAction } from './actions'; - interface PanningState { /** * Screen coordinate vector representing the starting point when panning. 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 c5e40b27ff1489..19744cf41ed779 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react'; import { Store } from 'redux'; import { Provider, useSelector, useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; import { ResolverState, ResolverAction, Vector2 } from '../types'; @@ -21,11 +20,9 @@ export const AppRoot = React.memo(({ store }: { store: Store useDispatch>(); - const Diagnostic = styled( React.memo(({ className }: { className?: string }) => { - const dispatch = useResolverDispatch(); + const dispatch: (action: ResolverAction) => unknown = useDispatch(); const [ref, setRef] = useState(null); From 0a1df7de4fbbd163fcd8538d6d7ae3a7adc8ebd4 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 19 Dec 2019 15:41:06 -0500 Subject: [PATCH 42/86] change all the interface imports again as usual --- .../public/embeddables/resolver/embeddable.tsx | 14 +++++--------- .../public/embeddables/resolver/factory.ts | 16 ++++++---------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index 35b995848a69a8..814efc50bfb978 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -13,17 +13,13 @@ import { IContainer, Embeddable, } from '../../../../../../src/plugins/embeddable/public'; -import { HttpServiceBase } from '../../../../../../src/core/public'; +import { HttpSetup } from '../../../../../../src/core/public'; export class ResolverEmbeddable extends Embeddable { public readonly type = 'resolver'; - private httpServiceBase: HttpServiceBase; + private httpService: HttpSetup; private lastRenderTarget?: Element; - constructor( - initialInput: EmbeddableInput, - httpServiceBase: HttpServiceBase, - parent?: IContainer - ) { + constructor(initialInput: EmbeddableInput, httpService: HttpSetup, parent?: IContainer) { super( // Input state is irrelevant to this embeddable, just pass it along. initialInput, @@ -33,7 +29,7 @@ export class ResolverEmbeddable extends Embeddable { // Optional parent component, this embeddable can optionally be rendered inside a container. parent ); - this.httpServiceBase = httpServiceBase; + this.httpService = httpService; } public render(node: HTMLElement) { @@ -42,7 +38,7 @@ export class ResolverEmbeddable extends Embeddable { } this.lastRenderTarget = node; // TODO, figure out how to destroy middleware - const { store } = storeFactory({ httpServiceBase: this.httpServiceBase }); + const { store } = storeFactory({ httpServiceBase: 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 63a0a2021758a4..5a4f33f3fbf7e7 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -5,21 +5,17 @@ */ import { i18n } from '@kbn/i18n'; -import { HttpServiceBase } from 'kibana/public'; -import { - EmbeddableFactory, - EmbeddableInput, - IContainer, -} from '../../../../../../src/plugins/embeddable/public'; +import { EmbeddableFactory, EmbeddableInput, IContainer } from 'src/plugins/embeddable/public'; +import { HttpSetup } from 'kibana/public'; import { ResolverEmbeddable } from './'; export class ResolverEmbeddableFactory extends EmbeddableFactory { public readonly type = 'resolver'; - private httpServiceBase: HttpServiceBase; + private httpService: HttpSetup; - constructor(httpServiceBase: HttpServiceBase) { + constructor(httpService: HttpSetup) { super(); - this.httpServiceBase = httpServiceBase; + this.httpService = httpService; } public isEditable() { @@ -27,7 +23,7 @@ export class ResolverEmbeddableFactory extends EmbeddableFactory { } public async create(initialInput: EmbeddableInput, parent?: IContainer) { - return new ResolverEmbeddable(initialInput, this.httpServiceBase, parent); + return new ResolverEmbeddable(initialInput, this.httpService, parent); } public getDisplayName() { From 52f79c678eac9bbc8173951ec14af11c617ad3df Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 20 Dec 2019 13:55:23 -0500 Subject: [PATCH 43/86] cleanup fix some type issues remove some excess actions document some stuff --- .../embeddables/resolver/embeddable.tsx | 2 +- .../public/embeddables/resolver/factory.ts | 8 +- .../resolver/store/camera/action.ts | 26 +--- .../resolver/store/camera/panning.test.ts | 2 +- .../resolver/store/camera/reducer.ts | 32 ++-- .../resolver/store/camera/zooming.test.ts | 5 +- .../embeddables/resolver/store/index.ts | 9 +- .../embeddables/resolver/view/index.tsx | 143 +++++++++--------- 8 files changed, 112 insertions(+), 115 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index 814efc50bfb978..a303b3418c040f 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 5a4f33f3fbf7e7..7925aea479a5fa 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 3251e624484bf9..58b72b911355c6 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 d7d6a01d64b2d3..361b2f59620dda 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 467ba0efea5195..fb2f137ee77abb 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 bdcd6dfd07c39f..3429ce78cde1d3 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 7a435b8b1e3bb4..3b3f51e9821c5c 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 19744cf41ed779..67812a5eebc6d8 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]); +} From f87e2b4b0051b67ada47de9126439833b696ad52 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 20 Dec 2019 15:13:45 -0500 Subject: [PATCH 44/86] move diagnostic dot to new file --- .../resolver/view/diagnostic_dot.tsx | 41 +++++++++++++++++++ .../embeddables/resolver/view/index.tsx | 32 +-------------- 2 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx new file mode 100644 index 00000000000000..6c8266cdc566d4 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { useSelector } from 'react-redux'; +import { applyMatrix3 } from '../lib/vector2'; +import * as selectors from '../store/selectors'; + +export 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; +`; 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 67812a5eebc6d8..6b4f441b83243d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; +import { DiagnosticDot } from './diagnostic_dot'; export const AppRoot = React.memo(({ store }: { store: Store }) => { return ( @@ -158,37 +159,6 @@ const Diagnostic = styled( flex-grow: 1; 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. From 5863434ea5ed9bacf8203e6a7b64adbcce2bc6c7 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 20 Dec 2019 15:14:52 -0500 Subject: [PATCH 45/86] move autoupdating client rect to new file, refactor it --- .../embeddables/resolver/view/index.tsx | 40 ++-------------- .../view/use_autoupdating_client_rect.tsx | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx 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 6b4f441b83243d..905956ee4a1c5e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; +import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect'; import { DiagnosticDot } from './diagnostic_dot'; export const AppRoot = React.memo(({ store }: { store: Store }) => { @@ -33,7 +34,7 @@ const Diagnostic = styled( const relativeCoordinatesFromMouseEvent = useCallback( (event: { clientX: number; clientY: number }): null | [number, number] => { - if (elementBoundingClientRect === undefined) { + if (elementBoundingClientRect === null) { return null; } return [ @@ -45,7 +46,7 @@ const Diagnostic = styled( ); useEffect(() => { - if (elementBoundingClientRect !== undefined) { + if (elementBoundingClientRect !== null) { dispatch({ type: 'userSetRasterSize', payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], @@ -91,7 +92,7 @@ const Diagnostic = styled( (event: WheelEvent) => { // we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height if ( - elementBoundingClientRect !== undefined && + elementBoundingClientRect !== null && event.ctrlKey && event.deltaY !== 0 && event.deltaMode === 0 @@ -159,39 +160,6 @@ const Diagnostic = styled( flex-grow: 1; position: relative; `; -/** - * 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. - * Updates automatically when the window resizes. TODO: better Englishe here - */ -function useAutoUpdatingClientRect(): [DOMRect | undefined, (node: Element | null) => void] { - const [rect, setRect] = useState(); - const nodeRef = useRef(); - - const ref = useCallback((node: Element | null) => { - // why do we have to deal /w both null and undefined? TODO - nodeRef.current = node === null ? undefined : node; - if (node !== null) { - setRect(node.getBoundingClientRect()); - } - }, []); - - // TODO, this isn't really a concern of Resolver. - // The parent should inform resolver that it needs to rerender - useEffect(() => { - window.addEventListener('resize', handler, { passive: true }); - return () => { - window.removeEventListener('resize', handler); - }; - function handler() { - if (nodeRef.current !== undefined) { - setRect(nodeRef.current.getBoundingClientRect()); - } - } - }, []); - return [rect, ref]; -} - /** * 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. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx new file mode 100644 index 00000000000000..31a9d5e3eb45ff --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useState, useEffect, useRef } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; + +/** Built in typescript DOM libs and the ResizeObserver polyfill have incompatible definitions of DOMRectReadOnly so we use this basic one + */ +interface BasicDOMRect { + x: number; + y: number; + width: number; + height: number; +} + +/** + * 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. + * Updates automatically when the window resizes. TODO: better Englishe here + */ +export function useAutoUpdatingClientRect(): [BasicDOMRect | null, (node: Element | null) => void] { + const [rect, setRect] = useState(null); + const nodeRef = useRef(null); + const ref = useCallback((node: Element | null) => { + nodeRef.current = node; + if (node !== null) { + setRect(node.getBoundingClientRect()); + } + }, []); + useEffect(() => { + if (nodeRef.current !== null) { + const resizeObserver = new ResizeObserver(entries => { + if (nodeRef.current !== null && nodeRef.current === entries[0].target) { + setRect(nodeRef.current.getBoundingClientRect()); + } + }); + resizeObserver.observe(nodeRef.current); + return () => { + resizeObserver.disconnect(); + }; + } + }, [nodeRef]); + return [rect, ref]; +} From b2950c2b9a4e370ba28f03766da52a60dd0438c7 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 20 Dec 2019 15:15:12 -0500 Subject: [PATCH 46/86] move nonpassive wheel hook to new file --- .../embeddables/resolver/view/index.tsx | 22 ++-------------- .../view/use_nonpassive_wheel_handler.tsx | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx 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 905956ee4a1c5e..63788304dc0c07 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react'; +import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { Store } from 'redux'; import { Provider, useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; @@ -12,6 +12,7 @@ import { applyMatrix3 } from '../lib/vector2'; import { ResolverState, ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect'; +import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler'; import { DiagnosticDot } from './diagnostic_dot'; export const AppRoot = React.memo(({ store }: { store: Store }) => { @@ -160,22 +161,3 @@ const Diagnostic = styled( flex-grow: 1; position: relative; `; -/** - * 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]); -} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx new file mode 100644 index 00000000000000..a0738bcf4d14c6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; +/** + * 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. + */ +export function useNonPassiveWheelHandler( + handler: (event: WheelEvent) => void, + elementRef: HTMLElement | null +) { + useEffect(() => { + if (elementRef !== null) { + elementRef.addEventListener('wheel', handler); + return () => { + elementRef.removeEventListener('wheel', handler); + }; + } + }, [elementRef, handler]); +} From 6c500e34356e74bc3d46a8c48cad181c7e145bd7 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 20 Dec 2019 15:06:48 -0500 Subject: [PATCH 47/86] trying to fix dumb zoom issue but dont know whats wrong --- .../resolver/store/camera/reducer.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) 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 fb2f137ee77abb..cda0f5f1fb53ce 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 @@ -45,6 +45,7 @@ export const cameraReducer: Reducer = ( ); const matrix = inverseProjectionMatrix(stateWithNewScaling); const worldCoordinateThereNow = applyMatrix3(rasterOfLastFocusedWorldCoordinates, matrix); + // TODO, use vector subtraction method const delta = [ worldCoordinateThereNow[0] - state.latestFocusedWorldCoordinates[0], worldCoordinateThereNow[1] - state.latestFocusedWorldCoordinates[1], @@ -94,15 +95,8 @@ export const cameraReducer: Reducer = ( rasterSize: action.payload, }; } else if (action.type === 'userMovedPointer') { - return { + const stateWithUpdatedPanning = { ...state, - /** - * 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 */ @@ -113,6 +107,19 @@ export const cameraReducer: Reducer = ( } : state.panning, }; + return { + ...stateWithUpdatedPanning, + /** + * 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(stateWithUpdatedPanning) + ), + }; } else { return state; } From 9eca46eb3b65f176ddf14bdb20f01589d167766d Mon Sep 17 00:00:00 2001 From: oatkiller Date: Fri, 20 Dec 2019 16:20:01 -0500 Subject: [PATCH 48/86] improve types and docs for useAutoupdatingClientRect --- .../view/use_autoupdating_client_rect.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx index 31a9d5e3eb45ff..5f13995de1c2ad 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx @@ -7,22 +7,18 @@ import { useCallback, useState, useEffect, useRef } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; -/** Built in typescript DOM libs and the ResizeObserver polyfill have incompatible definitions of DOMRectReadOnly so we use this basic one - */ -interface BasicDOMRect { - x: number; - y: number; - width: number; - height: number; -} - /** - * 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. - * Updates automatically when the window resizes. TODO: better Englishe here + * Returns a nullable DOMRect and a ref callback. Pass the refCallback to the + * `ref` property of a native element and this hook will return a DOMRect for + * it by calling `getBoundingClientRect`. This hook will observe the element + * with a resize observer and call getBoundingClientRect again after resizes. + * + * Note that the changes to the position of the element aren't automatically + * tracked. So if the element's position moves for some reason, be sure to + * handle that. */ -export function useAutoUpdatingClientRect(): [BasicDOMRect | null, (node: Element | null) => void] { - const [rect, setRect] = useState(null); +export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { + const [rect, setRect] = useState(null); const nodeRef = useRef(null); const ref = useCallback((node: Element | null) => { nodeRef.current = node; From f7c66d4931bf926f1e5f3a6c99ca7d6a61bcdbaa Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 2 Jan 2020 16:55:08 -0500 Subject: [PATCH 49/86] fix tests, imports. also remove `http` concern from Resolver for now --- .../embeddables/resolver/embeddable.tsx | 23 ++-------------- .../public/embeddables/resolver/factory.ts | 9 +------ .../embeddables/resolver/store/index.ts | 26 ++++++++++++++----- .../resolver/view/diagnostic_dot.tsx | 1 + .../embeddables/resolver/view/index.tsx | 10 ++++--- x-pack/plugins/endpoint/public/plugin.ts | 2 +- 6 files changed, 32 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index a303b3418c040f..9539162f9cfb64 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -8,37 +8,18 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { AppRoot } from './view'; import { storeFactory } from './store'; -import { - EmbeddableInput, - IContainer, - Embeddable, -} from '../../../../../../src/plugins/embeddable/public'; -import { HttpSetup } from '../../../../../../src/core/public'; +import { Embeddable } from '../../../../../../src/plugins/embeddable/public'; export class ResolverEmbeddable extends Embeddable { public readonly type = 'resolver'; - private httpService: HttpSetup; private lastRenderTarget?: Element; - constructor(initialInput: EmbeddableInput, httpService: HttpSetup, parent?: IContainer) { - super( - // Input state is irrelevant to this embeddable, just pass it along. - initialInput, - // Initial output state - this embeddable does not do anything with output, so just - // pass along an empty object. - {}, - // Optional parent component, this embeddable can optionally be rendered inside a container. - parent - ); - this.httpService = httpService; - } public render(node: HTMLElement) { if (this.lastRenderTarget !== undefined) { ReactDOM.unmountComponentAtNode(this.lastRenderTarget); } this.lastRenderTarget = node; - // TODO, figure out how to destroy middleware - const { store } = storeFactory({ httpService: this.httpService }); + const { store } = storeFactory(); 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 7925aea479a5fa..f5d1aad93ed574 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -5,7 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { HttpSetup } from 'kibana/public'; import { EmbeddableFactory, IContainer, @@ -15,19 +14,13 @@ import { ResolverEmbeddable } from './embeddable'; export class ResolverEmbeddableFactory extends EmbeddableFactory { public readonly type = 'resolver'; - private httpService: HttpSetup; - - constructor(httpService: HttpSetup) { - super(); - this.httpService = httpService; - } public isEditable() { return true; } public async create(initialInput: EmbeddableInput, parent?: IContainer) { - return new ResolverEmbeddable(initialInput, this.httpService, parent); + return new ResolverEmbeddable(initialInput, {}, parent); } public getDisplayName() { 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 3b3f51e9821c5c..d043453a8e4cdf 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -6,15 +6,29 @@ import { createStore, StoreEnhancer } from 'redux'; import { ResolverAction } from '../types'; -import { HttpSetup } from '../../../../../../../src/core/public'; import { resolverReducer } from './reducer'; -export const storeFactory = (_dependencies: { httpService: HttpSetup }) => { +export const storeFactory = () => { + /** + * Redux Devtools extension exposes itself via a property on the global object. + * This interface can be used to cast `window` to a type that may expose Redux Devtools. + */ interface SomethingThatMightHaveReduxDevTools { - __REDUX_DEVTOOLS_EXTENSION__?: (options?: { - name?: string; - actionsBlacklist: readonly string[]; - }) => StoreEnhancer; + __REDUX_DEVTOOLS_EXTENSION__?: (options?: PartialReduxDevToolsOptions) => StoreEnhancer; + } + + /** + * Some of the options that can be passed when configuring Redux Devtools. + */ + interface PartialReduxDevToolsOptions { + /** + * A name for this store + */ + name?: string; + /** + * A list of action types to ignore. This is used to ignore high frequency events created by a mousemove handler + */ + actionsBlacklist?: readonly string[]; } const windowWhichMightHaveReduxDevTools = window as SomethingThatMightHaveReduxDevTools; // Make sure blacklisted action types are valid diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx index 6c8266cdc566d4..b6e70c18a46285 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx @@ -7,6 +7,7 @@ import React from 'react'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; +import { Vector2 } from '../types'; import { applyMatrix3 } from '../lib/vector2'; import * as selectors from '../store/selectors'; 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 63788304dc0c07..73667eff418445 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -8,8 +8,7 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { Store } from 'redux'; import { Provider, useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { applyMatrix3 } from '../lib/vector2'; -import { ResolverState, ResolverAction, Vector2 } from '../types'; +import { ResolverState, ResolverAction } from '../types'; import * as selectors from '../store/selectors'; import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect'; import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler'; @@ -148,7 +147,12 @@ const Diagnostic = styled( useNonPassiveWheelHandler(handleWheel, ref); return ( -
+
{dotPositions.map((worldPosition, index) => ( ))} diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 219477698c2642..355364253b2a52 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -38,7 +38,7 @@ export class EndpointPlugin }, }); - const resolverEmbeddableFactory = new ResolverEmbeddableFactory(core.http); + const resolverEmbeddableFactory = new ResolverEmbeddableFactory(); plugins.embeddable.registerEmbeddableFactory( resolverEmbeddableFactory.type, From b153b8c1076863d04a081bd4582ccaaea5771795 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 6 Jan 2020 17:31:37 -0500 Subject: [PATCH 50/86] rename userSetPositionOfCamera --- .../embeddables/resolver/store/camera/action.ts | 12 ++++++------ .../store/camera/inverse_projection_matrix.test.ts | 4 ++-- .../resolver/store/camera/projection_matrix.test.ts | 4 ++-- .../embeddables/resolver/store/camera/reducer.ts | 2 +- .../resolver/store/camera/zooming.test.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) 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 58b72b911355c6..ae45120c292191 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 @@ -30,11 +30,11 @@ interface UserSetRasterSize { readonly payload: Vector2; } -// TODO, fix and rename this. or remove it -// this is used to directly set the transform of the camera, -// it should be named something like 'userSetPositionOfCamera'. It should work in conjunction with panning (it doesn't, or at least its not tested or tried.) -interface UserSetPanningOffset { - readonly type: 'userSetPanningOffset'; +/** + * This is currently only used in tests. The 'back to center' button will use this action, and more tests around its behavior will need to be added. + */ +interface UserSetPositionOfCamera { + readonly type: 'userSetPositionOfCamera'; /** * The world transform of the camera */ @@ -71,7 +71,7 @@ interface UserMovedPointer { export type CameraAction = | UserScaled | UserSetRasterSize - | UserSetPanningOffset + | UserSetPositionOfCamera | UserStartedPanning | UserStoppedPanning | UserCanceledPanning diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts index 9afb3f84882094..3d555b63d83926 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts @@ -68,7 +68,7 @@ describe('inverseProjectionMatrix', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPanningOffset', payload: [-50, -50] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-50, -50] }; store.dispatch(action); }); it('should convert 100,150 in raster space to 0,0 (center) in world space', () => { @@ -83,7 +83,7 @@ describe('inverseProjectionMatrix', () => { }); describe('when the user has panned to the right by 350 and up by 250', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPanningOffset', payload: [-350, -250] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-350, -250] }; store.dispatch(action); }); describe('when the user has scaled to 2', () => { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts index 8849abcf427ac0..025c436a957e89 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts @@ -65,7 +65,7 @@ describe('projectionMatrix', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPanningOffset', payload: [-50, -50] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-50, -50] }; store.dispatch(action); }); it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { @@ -81,7 +81,7 @@ describe('projectionMatrix', () => { describe('when the user has panned to the right by 350 and up by 250', () => { beforeEach(() => { const action: CameraAction = { - type: 'userSetPanningOffset', + type: 'userSetPositionOfCamera', payload: [-350, -250], }; store.dispatch(action); 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 cda0f5f1fb53ce..a11b5a193c1e9a 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 @@ -61,7 +61,7 @@ export const cameraReducer: Reducer = ( } else { return stateWithNewScaling; } - } else if (action.type === 'userSetPanningOffset') { + } else if (action.type === 'userSetPositionOfCamera') { return { ...state, translationNotCountingCurrentPanning: action.payload, 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 3429ce78cde1d3..0dcd7bfc78e19f 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 @@ -106,7 +106,7 @@ describe('zooming', () => { }); describe('when the user pans right by 100 pixels', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPanningOffset', payload: [-100, 0] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-100, 0] }; store.dispatch(action); }); it( From 71399aa87598ff4a4531f226de078608722def18 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 7 Jan 2020 16:22:23 -0500 Subject: [PATCH 51/86] remove cancel panning concept --- .../resolver/store/camera/action.ts | 5 ---- .../resolver/store/camera/reducer.ts | 30 +++++++++++-------- .../resolver/store/camera/zooming.test.ts | 9 +++--- 3 files changed, 22 insertions(+), 22 deletions(-) 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 ae45120c292191..b21b79e84f7417 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 @@ -55,10 +55,6 @@ interface UserStoppedPanning { readonly type: 'userStoppedPanning'; } -interface UserCanceledPanning { - readonly type: 'userCanceledPanning'; -} - interface UserMovedPointer { readonly type: 'userMovedPointer'; /** @@ -74,6 +70,5 @@ export type CameraAction = | UserSetPositionOfCamera | UserStartedPanning | UserStoppedPanning - | UserCanceledPanning | UserZoomed | UserMovedPointer; 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 a11b5a193c1e9a..7009c9b8c34b1c 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 @@ -5,7 +5,7 @@ */ import { Reducer } from 'redux'; -import { applyMatrix3 } from '../../lib/vector2'; +import { applyMatrix3, subtract } from '../../lib/vector2'; import { userIsPanning, translation, projectionMatrix, inverseProjectionMatrix } from './selectors'; import { clamp } from '../../lib/math'; @@ -31,25 +31,34 @@ export const cameraReducer: Reducer = ( scaling: [clamp(deltaX, 0.1, 3), clamp(deltaY, 0.1, 3)], }; } else if (action.type === 'userZoomed') { + /** + * When the user zooms we change the scale. Limit the change in scale so that we aren't liable for supporting crazy values (e.g. infinity or negative scale.) + */ const newScaleX = clamp(state.scaling[0] + action.payload, 0.1, 3); const newScaleY = clamp(state.scaling[1] + action.payload, 0.1, 3); + const stateWithNewScaling: CameraState = { ...state, scaling: [newScaleX, newScaleY], }; - // TODO, test that asserts that this behavior doesn't happen when user is panning - if (state.latestFocusedWorldCoordinates !== null && userIsPanning(state) === false) { + + /** + * Zooming fundamentally just changes the scale, but that would always zoom in (or out) around the center of the map. The user might be interested in + * something else, like a node. If the user has moved their pointer on to the map, we can keep the pointer over the same point in the map by adjusting the + * panning when we zoom. + * + * You can see this in action by moving your pointer over a node that isn't directly in the center of the map and then changing the zoom level. Do it by + * using CTRL and the mousewheel, or by pinching the trackpad on a Mac. The node will stay under your mouse cursor and other things in the map will get + * nearer or further from the mouse cursor. This lets you keep your context when changing zoom levels. + */ + if (state.latestFocusedWorldCoordinates !== null) { const rasterOfLastFocusedWorldCoordinates = applyMatrix3( state.latestFocusedWorldCoordinates, projectionMatrix(state) ); const matrix = inverseProjectionMatrix(stateWithNewScaling); const worldCoordinateThereNow = applyMatrix3(rasterOfLastFocusedWorldCoordinates, matrix); - // TODO, use vector subtraction method - const delta = [ - worldCoordinateThereNow[0] - state.latestFocusedWorldCoordinates[0], - worldCoordinateThereNow[1] - state.latestFocusedWorldCoordinates[1], - ]; + const delta = subtract(worldCoordinateThereNow, state.latestFocusedWorldCoordinates); return { ...stateWithNewScaling, @@ -84,11 +93,6 @@ export const cameraReducer: Reducer = ( } else { return state; } - } else if (action.type === 'userCanceledPanning') { - return { - ...state, - panning: undefined, - }; } else if (action.type === 'userSetRasterSize') { 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 0dcd7bfc78e19f..a04ca8376c9b19 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,7 +75,6 @@ describe('zooming', () => { }); describe('when the user has moved their mouse to the raster position 200, 50', () => { beforeEach(() => { - // TODO update action const action: CameraAction = { type: 'userMovedPointer', payload: [200, 50], @@ -84,9 +83,11 @@ describe('zooming', () => { }); it('should have focused the world position 50, 50', () => { const coords = store.getState().latestFocusedWorldCoordinates; - expect(coords).not.toBeNull(); - // TODO, revisit this - expectVectorsToBeClose(coords!, [50, 50]); + if (coords !== null) { + expectVectorsToBeClose(coords, [50, 50]); + } else { + throw new Error('coords should not have been null'); + } }); describe('when the user zooms in by 0.5 zoom units', () => { beforeEach(() => { From 48df3c36f8aebf9608158b2d8ec34c48bf63838d Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 7 Jan 2020 17:00:31 -0500 Subject: [PATCH 52/86] comments --- .../endpoint/public/embeddables/resolver/view/index.tsx | 1 - .../resolver/view/use_autoupdating_client_rect.tsx | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) 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 73667eff418445..d8a155ccf7295f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -160,7 +160,6 @@ const Diagnostic = styled( ); }) )` - /* TODO, this is not a concern of Resolver. its parent needs to do this probably? */ display: flex; flex-grow: 1; position: relative; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx index 5f13995de1c2ad..54bc8ad1521632 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx @@ -16,6 +16,11 @@ import ResizeObserver from 'resize-observer-polyfill'; * Note that the changes to the position of the element aren't automatically * tracked. So if the element's position moves for some reason, be sure to * handle that. + * + * Future performance improvement ideas: + * If `getBoundingClientRect` calls are happening frequently and this is causing performance issues + * we could call getBoundingClientRect less and instead invalidate a cached version of it. + * When needed, we could call `getBoundingClientRect` and store it. TODO more deets */ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { const [rect, setRect] = useState(null); From d163e1ac1d725891dcd05dfd8dd95dc65f9ef736 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 7 Jan 2020 17:08:36 -0500 Subject: [PATCH 53/86] rename Resolver component? --- .../endpoint/public/embeddables/resolver/view/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d8a155ccf7295f..eff7995badffa0 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -17,12 +17,12 @@ import { DiagnosticDot } from './diagnostic_dot'; export const AppRoot = React.memo(({ store }: { store: Store }) => { return ( - + ); }); -const Diagnostic = styled( +const Resolver = styled( React.memo(({ className }: { className?: string }) => { const dispatch: (action: ResolverAction) => unknown = useDispatch(); From 1dc1e1802e59ec5c118daf5ec9138304e4aa76c2 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 7 Jan 2020 17:56:22 -0500 Subject: [PATCH 54/86] more comments --- .../resolver/store/camera/reducer.ts | 35 ++++++++++++++-- .../resolver/store/camera/selectors.ts | 41 ++++++++++++++++--- .../resolver/store/camera/test_helpers.ts | 6 +++ .../embeddables/resolver/store/selectors.ts | 21 ++++++++++ 4 files changed, 94 insertions(+), 9 deletions(-) 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 7009c9b8c34b1c..b4e05b8521e604 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 @@ -20,22 +20,38 @@ function initialState(): CameraState { }; } +/** + * The minimum allowed value for the camera scale. This is the least scale that we will ever render something at. + */ +const minimumScale = 0.1; + +/** + * The maximum allowed value for the camera scale. This is greatest scale that we will ever render something at. + */ +const maximumScale = 3; + export const cameraReducer: Reducer = ( state = initialState(), action ) => { if (action.type === 'userScaled') { + /** + * Handle the scale being explicitly set, for example by a 'reset zoom' feature, or by a range slider with exact scale values + */ const [deltaX, deltaY] = action.payload; return { ...state, - scaling: [clamp(deltaX, 0.1, 3), clamp(deltaY, 0.1, 3)], + scaling: [ + clamp(deltaX, minimumScale, maximumScale), + clamp(deltaY, minimumScale, maximumScale), + ], }; } else if (action.type === 'userZoomed') { /** * When the user zooms we change the scale. Limit the change in scale so that we aren't liable for supporting crazy values (e.g. infinity or negative scale.) */ - const newScaleX = clamp(state.scaling[0] + action.payload, 0.1, 3); - const newScaleY = clamp(state.scaling[1] + action.payload, 0.1, 3); + const newScaleX = clamp(state.scaling[0] + action.payload, minimumScale, maximumScale); + const newScaleY = clamp(state.scaling[1] + action.payload, minimumScale, maximumScale); const stateWithNewScaling: CameraState = { ...state, @@ -71,11 +87,17 @@ export const cameraReducer: Reducer = ( return stateWithNewScaling; } } else if (action.type === 'userSetPositionOfCamera') { + /** + * Handle the case where the position of the camera is explicitly set, for example by a 'back to center' feature. + */ return { ...state, translationNotCountingCurrentPanning: action.payload, }; } else if (action.type === 'userStartedPanning') { + /** + * When the user begins panning with a mousedown event we mark the starting position for later comparisons. + */ return { ...state, panning: { @@ -84,6 +106,9 @@ export const cameraReducer: Reducer = ( }, }; } else if (action.type === 'userStoppedPanning') { + /** + * When the user stops panning (by letting up on the mouse) we calculate the new translation of the camera. + */ if (userIsPanning(state)) { return { ...state, @@ -94,6 +119,10 @@ export const cameraReducer: Reducer = ( return state; } } else if (action.type === 'userSetRasterSize') { + /** + * Handle resizes of the Resolver component. We need to know the size in order to convert between screen + * and world coordinates. + */ return { ...state, rasterSize: action.payload, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 3f6300f4b989c7..ca62d7e012976e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -14,7 +14,7 @@ import { translationTransformation, } from '../../lib/transformation'; -interface ClippingPlane { +interface ClippingPlanes { renderWidth: number; renderHeight: number; clippingPlaneRight: number; @@ -23,8 +23,11 @@ interface ClippingPlane { clippingPlaneBottom: number; } +/** + * The viewable area in the Resolver map, in world coordinates. + */ export function viewableBoundingBox(state: CameraState): AABB { - const { renderWidth, renderHeight } = clippingPlane(state); + const { renderWidth, renderHeight } = clippingPlanes(state); const matrix = inverseProjectionMatrix(state); return { minimum: applyMatrix3([0, renderHeight], matrix), @@ -32,7 +35,10 @@ export function viewableBoundingBox(state: CameraState): AABB { }; } -function clippingPlane(state: CameraState): ClippingPlane { +/** + * The 2D clipping planes used for the orthographic projection. See https://en.wikipedia.org/wiki/Orthographic_projection + */ +function clippingPlanes(state: CameraState): ClippingPlanes { const renderWidth = state.rasterSize[0]; const renderHeight = state.rasterSize[1]; const clippingPlaneRight = renderWidth / 2 / state.scaling[0]; @@ -49,7 +55,8 @@ function clippingPlane(state: CameraState): ClippingPlane { } /** - * https://en.wikipedia.org/wiki/Orthographic_projection + * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. + * See https://en.wikipedia.org/wiki/Orthographic_projection */ export const projectionMatrix: (state: CameraState) => Matrix3 = state => { const { @@ -59,7 +66,7 @@ export const projectionMatrix: (state: CameraState) => Matrix3 = state => { clippingPlaneTop, clippingPlaneLeft, clippingPlaneBottom, - } = clippingPlane(state); + } = clippingPlanes(state); return multiply( // 5. convert from 0->2 to 0->rasterWidth (or height) @@ -86,6 +93,18 @@ export const projectionMatrix: (state: CameraState) => Matrix3 = state => { ); }; +/** + * The camera has a translation value (not counting any current panning.) This is initialized to (0, 0) and + * updating any time panning ends. + * + * When the user is panning, we keep the initial position of the pointer and the current position of the + * pointer. The difference between these values equals the panning vector. + * + * When the user is panning, the translation of the camera is found by adding the panning vector to the + * translationNotCountingCurrentPanning. + * + * We could update the translation as the user moved the mouse but floating point drift (round-off error) could occur. + */ export function translation(state: CameraState): Vector2 { if (state.panning) { return add( @@ -101,6 +120,10 @@ export function translation(state: CameraState): Vector2 { } } +/** + * A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates. + * See https://en.wikipedia.org/wiki/Orthographic_projection + */ export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => { const { renderWidth, @@ -109,7 +132,7 @@ export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => clippingPlaneTop, clippingPlaneLeft, clippingPlaneBottom, - } = clippingPlane(state); + } = clippingPlanes(state); const [translationX, translationY] = translation(state); @@ -145,6 +168,12 @@ export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => ); }; +/** + * The scale by which world values are scaled when rendered. + */ export const scale = (state: CameraState): Vector2 => state.scaling; +/** + * Whether or not the user is current panning the map. + */ export const userIsPanning = (state: CameraState): boolean => state.panning !== undefined; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts index c906e621873e68..fd446c42116a40 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts @@ -10,11 +10,17 @@ import { CameraState, Vector2 } from '../../types'; type CameraStore = Store; +/** + * Dispatches a 'userScaled' action. + */ export function userScaled(store: CameraStore, scalingValue: [number, number]): void { const action: CameraAction = { type: 'userScaled', payload: scalingValue }; store.dispatch(action); } +/** + * Used to assert that two Vector2s are close to each other (accounting for round-off errors.) + */ export function expectVectorsToBeClose(first: Vector2, second: Vector2): void { expect(first[0]).toBeCloseTo(second[0]); expect(first[1]).toBeCloseTo(second[1]); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 734aaf0041984c..402e0ec8ca0d9d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -7,24 +7,45 @@ import * as cameraSelectors from './camera/selectors'; import { ResolverState } from '../types'; +/** + * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. + * See https://en.wikipedia.org/wiki/Orthographic_projection + */ export const projectionMatrix = composeSelectors( cameraStateSelector, cameraSelectors.projectionMatrix ); +/** + * A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates. + * See https://en.wikipedia.org/wiki/Orthographic_projection + */ export const inverseProjectionMatrix = composeSelectors( cameraStateSelector, cameraSelectors.inverseProjectionMatrix ); +/** + * The scale by which world values are scaled when rendered. + */ export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale); +/** + * Whether or not the user is current panning the map. + */ export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelectors.userIsPanning); +/** + * Returns the camera state from within ResolverState + */ function cameraStateSelector(state: ResolverState) { return state.camera; } +/** + * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a + * concern-specific selector. `selector` should return the concern-specific state. + */ function composeSelectors( selector: (state: OuterState) => InnerState, secondSelector: (state: InnerState) => ReturnValue From ae4b9e74d7f9d4d7f7d8d16255d92e518e584fd6 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 8 Jan 2020 10:29:32 -0500 Subject: [PATCH 55/86] completely remove canel panning feature --- .../embeddables/resolver/store/camera/panning.test.ts | 9 --------- 1 file changed, 9 deletions(-) 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 361b2f59620dda..c09320182e3be0 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 @@ -55,15 +55,6 @@ describe('panning interaction', () => { translationShouldBeCloseTo([50, 50]); }); }); - describe('when the user then cancels panning', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userCanceledPanning' }; - store.dispatch(action); - }); - it('should have a translation of 0,0', () => { - translationShouldBeCloseTo([0, 0]); - }); - }); }); }); }); From 72d575b41ddf4f539bed1084258d1f708f33857f Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 8 Jan 2020 11:08:31 -0500 Subject: [PATCH 56/86] add comment explaining the camera --- .../embeddables/resolver/store/camera/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts index a12054496583dc..f5e7d73aa10d57 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts @@ -4,5 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * The 'camera' in Resolver models the visible area of the Resolver map. Resolver users + * can click and drag the Resolver element to pan the map. They can pinch the trackpad + * or use Ctrl-MouseWheel to _zoom_, which changes the scale. + * + * The camera considers the size of Resolver in pixels, and it considers any panning that + * has been done, and it considers the scale. With this information it maps points on + * the screen to points in Resolver's 'world'. Entities that are mapped in Resolver + * are positioned in these unitless 'world' coordinates, and where they show up (if at all) + * on the screen is determined by the camera. + * + * In the future, we may cull entities before rendering them to the DOM. Entities that + * would not be in the camera's viewport would be ignored. + */ export { cameraReducer } from './reducer'; export { CameraAction } from './action'; From bfd22848bab2c1e82cccb54f09ab78ef3feb84bf Mon Sep 17 00:00:00 2001 From: Pedro Jaramillo Date: Mon, 23 Dec 2019 15:52:29 -0500 Subject: [PATCH 57/86] Basic layout process nodes and edges --- .../resolver/lib/tree_sequencers.ts | 28 + .../embeddables/resolver/store/data/sample.ts | 1596 +++++++++++++++++ .../resolver/store/data/selectors.ts | 302 ++++ .../embeddables/resolver/view/edge_line.tsx | 53 + .../embeddables/resolver/view/index.tsx | 14 +- .../resolver/view/process_event_dot.tsx | 54 + 6 files changed, 2045 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts new file mode 100644 index 00000000000000..1444e49a4d4672 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// TODO: type root and children +export function* depthFirstPreorder({ root, children }) { + const nodesToVisit = [root]; + while (nodesToVisit.length !== 0) { + const currentNode = nodesToVisit.shift(); + nodesToVisit.unshift(...(children(currentNode) || [])); + yield currentNode; + } +} + +export function* levelOrder({ root, children }) { + let level = [root]; + while (level.length !== 0) { + let nextLevel = []; + for (const node of level) { + yield node; + nextLevel.push(...(children(node) || [])); + } + level = nextLevel; + nextLevel = []; + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts new file mode 100644 index 00000000000000..92ada11bc3f7c8 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts @@ -0,0 +1,1596 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const sampleData = { + data: { + code: 200, + result: { + alert_id: 'a9834bf5-42c1-4039-83be-08c3ad3232b3', + bulk_task_id: null, + correlation_id: '7022e509-087e-493d-b02c-d88a206cd993', + created_at: '2019-09-24T03:17:36Z', + endpoint: { + ad_distinguished_name: + 'CN=ENDPOINT-W-1-07,OU=Desktops,OU=Workstations,OU=Computers_DEMO,DC=demo,DC=endgamelabs,DC=net', + ad_hostname: 'demo.endgamelabs.net', + display_operating_system: 'Windows 7 (SP1)', + hostname: 'ENDPOINT-W-1-07', + id: '39153006-0064-424b-99e9-4e21dcc00c2e', + ip_address: '172.31.27.17', + mac_address: '00:50:56:b1:b7:7b', + name: 'ENDPOINT-W-1-07', + operating_system: 'Windows 6.1 Service Pack 1', + status: 'monitored', + updated_at: '2019-09-24T01:48:47.960649+00:00', + }, + event_logging_search_request_count: 3, + family: 'collection', + investigation_id: null, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + message_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + metadata: { + chunk_id: 0, + correlation_id: '7022e509-087e-493d-b02c-d88a206cd993', + final: true, + message_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + origination_task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', + os_type: 'windows', + priority: 50, + result: { + local_code: 0, + local_msg: 'Success', + }, + semantic_version: '3.52.8', + task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', + type: 'collection', + }, + origination_task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', + pagination: { + backwards: false, + eof: false, + page_number: 3, + page_offset: 31666, + params: + 'eyJhbGVydF9pZCI6ICJhOTgzNGJmNS00MmMxLTQwMzktODNiZS0wOGMzYWQzMjMyYjMiLCAidGVtcGxhdGVfZmlsZSI6ICJwcm9jZXNzLWNvbnRleHQubHVhIiwgImNyaXRlcmlhIjogeyJwaWQiOiAxODA4LCAidW5pcXVlX3BpZCI6IDE4OTQzfX0=', + remaining_events: 0, + }, + pending_event_logging_search_request: false, + results_count: 807, + search_results: [ + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + _descendant_count: 6, + command_line: '', + depth: -5, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'already_running', + event_type_full: 'process_event', + integrity_level: 'system', + node_id: 1002, + opcode: 3, + pid: 4, + ppid: 0, + process_name: '', + process_path: '', + serial_event_id: 1002, + timestamp: 132137632670000000, + timestamp_utc: '2019-09-24 01:47:47Z', + unique_pid: 1002, + unique_ppid: 1001, + user_domain: 'NT AUTHORITY', + user_name: 'SYSTEM', + user_sid: 'S-1-5-18', + }, + event_timestamp: 132137632670000000, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + _descendant_count: 5, + command_line: '\\SystemRoot\\System32\\smss.exe', + depth: -4, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'already_running', + event_type_full: 'process_event', + integrity_level: 'system', + md5: '1911a3356fa3f77ccc825ccbac038c2a', + node_id: 1003, + opcode: 3, + original_file_name: 'smss.exe', + pid: 244, + ppid: 4, + process_name: 'smss.exe', + process_path: 'C:\\Windows\\System32\\smss.exe', + serial_event_id: 1003, + sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', + sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 1002, + timestamp: 132137632670000000, + timestamp_utc: '2019-09-24 01:47:47Z', + unique_pid: 1003, + unique_ppid: 1002, + user_domain: 'NT AUTHORITY', + user_name: 'SYSTEM', + user_sid: 'S-1-5-18', + }, + event_timestamp: 132137632670000000, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + _descendant_count: 4, + authentication_id: 999, + command_line: '\\SystemRoot\\System32\\smss.exe 00000000 00000048 ', + depth: -3, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'system', + md5: '1911a3356fa3f77ccc825ccbac038c2a', + node_id: 18643, + opcode: 1, + original_file_name: 'smss.exe', + parent_process_name: 'smss.exe', + parent_process_path: 'C:\\Windows\\System32\\smss.exe', + pid: 2364, + ppid: 244, + process_name: 'smss.exe', + process_path: 'C:\\Windows\\System32\\smss.exe', + serial_event_id: 18643, + sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', + sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 1003, + timestamp: 132137681960227504, + timestamp_utc: '2019-09-24 03:09:56Z', + unique_pid: 18643, + unique_ppid: 1003, + user_domain: 'NT AUTHORITY', + user_name: 'SYSTEM', + user_sid: 'S-1-5-18', + }, + event_timestamp: 132137681960227504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + _descendant_count: 3, + authentication_id: 999, + command_line: 'winlogon.exe', + depth: -2, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'system', + md5: '1151b1baa6f350b1db6598e0fea7c457', + node_id: 18645, + opcode: 1, + original_file_name: 'WINLOGON.EXE', + parent_process_name: 'smss.exe', + parent_process_path: 'C:\\Windows\\System32\\smss.exe', + pid: 3108, + ppid: 2364, + process_name: 'winlogon.exe', + process_path: 'C:\\Windows\\System32\\winlogon.exe', + serial_event_id: 18645, + sha1: '434856b834baf163c5ea4d26434eeae775a507fb', + sha256: 'b1506e0a7e826eff0f5252ef5026070c46e2235438403a9a24d73ee69c0b8a49', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18643, + timestamp: 132137681961163504, + timestamp_utc: '2019-09-24 03:09:56Z', + unique_pid: 18645, + unique_ppid: 18643, + user_domain: 'NT AUTHORITY', + user_name: 'SYSTEM', + user_sid: 'S-1-5-18', + }, + event_timestamp: 132137681961163504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + depth: -2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: '1911a3356fa3f77ccc825ccbac038c2a', + node_id: 18646, + opcode: 2, + original_file_name: 'smss.exe', + parent_process_name: 'smss.exe', + parent_process_path: 'C:\\Windows\\System32\\smss.exe', + pid: 2364, + ppid: 244, + process_name: 'smss.exe', + process_path: 'C:\\Windows\\System32\\smss.exe', + serial_event_id: 18646, + sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', + sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18643, + timestamp: 132137681961787504, + timestamp_utc: '2019-09-24 03:09:56Z', + unique_pid: 18643, + unique_ppid: 1003, + user_domain: 'NT AUTHORITY', + user_name: 'SYSTEM', + user_sid: 'S-1-5-18', + }, + event_timestamp: 132137681961787504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + _descendant_count: 1, + authentication_id: 4904488, + command_line: 'C:\\Windows\\system32\\userinit.exe', + depth: -1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'bafe84e637bf7388c96ef48d4d3fdd53', + node_id: 18833, + opcode: 1, + original_file_name: 'USERINIT.EXE', + parent_process_name: 'winlogon.exe', + parent_process_path: 'C:\\Windows\\System32\\winlogon.exe', + pid: 3560, + ppid: 3108, + process_name: 'userinit.exe', + process_path: 'C:\\Windows\\System32\\userinit.exe', + serial_event_id: 18833, + sha1: '47267f943f060e36604d56c8895a6eece063d9a1', + sha256: '11c194d9adce90027272c627d7fbf3ba5025ff0f7b26a8333f764e11e1382cf9', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18645, + timestamp: 132137681981287504, + timestamp_utc: '2019-09-24 03:09:58Z', + unique_pid: 18833, + unique_ppid: 18645, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137681981287504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + _descendant_count: 0, + authentication_id: 4904488, + command_line: 'C:\\Windows\\Explorer.EXE', + depth: 0, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'ac4c51eb24aa95b77f705ab159189e24', + node_id: 18943, + opcode: 1, + origin: true, + original_file_name: 'EXPLORER.EXE', + parent_process_name: 'userinit.exe', + parent_process_path: 'C:\\Windows\\System32\\userinit.exe', + pid: 1808, + ppid: 3560, + process_name: 'explorer.exe', + process_path: 'C:\\Windows\\explorer.exe', + serial_event_id: 18943, + sha1: '4583daf9442880204730fb2c8a060430640494b1', + sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18833, + timestamp: 132137681985655504, + timestamp_utc: '2019-09-24 03:09:58Z', + unique_pid: 18943, + unique_ppid: 18833, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137681985655504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe" -n vmusr', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: '8dc5ad50587b936f7f616738112bfd2a', + node_id: 19545, + opcode: 1, + original_file_name: 'vmtoolsd.exe', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3596, + ppid: 1808, + process_name: 'vmtoolsd.exe', + process_path: 'C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe', + serial_event_id: 19545, + sha1: '04479ea30943ec471a6a5ca4c0dc74b5ff496e9f', + sha256: 'd6d9f041da6f724bf69f48bbee3bf41295a0ed4dca715b1908c5f35bc8034d53', + signature_signer: 'VMware, Inc.', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137681999539504, + timestamp_utc: '2019-09-24 03:09:59Z', + unique_pid: 19545, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137681999539504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + depth: 0, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: 'bafe84e637bf7388c96ef48d4d3fdd53', + node_id: 20261, + opcode: 2, + original_file_name: 'USERINIT.EXE', + parent_process_name: 'winlogon.exe', + parent_process_path: 'C:\\Windows\\System32\\winlogon.exe', + pid: 3560, + ppid: 3108, + process_name: 'userinit.exe', + process_path: 'C:\\Windows\\System32\\userinit.exe', + serial_event_id: 20261, + sha1: '47267f943f060e36604d56c8895a6eece063d9a1', + sha256: '11c194d9adce90027272c627d7fbf3ba5025ff0f7b26a8333f764e11e1382cf9', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18833, + timestamp: 132137682277819504, + timestamp_utc: '2019-09-24 03:10:27Z', + unique_pid: 18833, + unique_ppid: 18645, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137682277819504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Windows\\explorer.exe" ', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'ac4c51eb24aa95b77f705ab159189e24', + node_id: 20303, + opcode: 1, + original_file_name: 'EXPLORER.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3124, + ppid: 1808, + process_name: 'explorer.exe', + process_path: 'C:\\Windows\\explorer.exe', + serial_event_id: 20303, + sha1: '4583daf9442880204730fb2c8a060430640494b1', + sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137682603979504, + timestamp_utc: '2019-09-24 03:11:00Z', + unique_pid: 20303, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137682603979504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 1, + md5: 'ac4c51eb24aa95b77f705ab159189e24', + node_id: 20310, + opcode: 2, + original_file_name: 'EXPLORER.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3124, + ppid: 1808, + process_name: 'explorer.exe', + process_path: 'C:\\Windows\\explorer.exe', + serial_event_id: 20310, + sha1: '4583daf9442880204730fb2c8a060430640494b1', + sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 20303, + timestamp: 132137682604229504, + timestamp_utc: '2019-09-24 03:11:00Z', + unique_pid: 20303, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137682604229504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Windows\\explorer.exe" ', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'ac4c51eb24aa95b77f705ab159189e24', + node_id: 20455, + opcode: 1, + original_file_name: 'EXPLORER.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3084, + ppid: 1808, + process_name: 'explorer.exe', + process_path: 'C:\\Windows\\explorer.exe', + serial_event_id: 20455, + sha1: '4583daf9442880204730fb2c8a060430640494b1', + sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137682773669504, + timestamp_utc: '2019-09-24 03:11:17Z', + unique_pid: 20455, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137682773669504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 1, + md5: 'ac4c51eb24aa95b77f705ab159189e24', + node_id: 20462, + opcode: 2, + original_file_name: 'EXPLORER.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3084, + ppid: 1808, + process_name: 'explorer.exe', + process_path: 'C:\\Windows\\explorer.exe', + serial_event_id: 20462, + sha1: '4583daf9442880204730fb2c8a060430640494b1', + sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 20455, + timestamp: 132137682774259504, + timestamp_utc: '2019-09-24 03:11:17Z', + unique_pid: 20455, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137682774259504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Windows\\System32\\cmd.exe" ', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: '5746bd7e255dd6a8afa06f7c42c1ba41', + node_id: 21120, + opcode: 1, + original_file_name: 'Cmd.Exe', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3280, + ppid: 1808, + process_name: 'cmd.exe', + process_path: 'C:\\Windows\\System32\\cmd.exe', + serial_event_id: 21120, + sha1: '0f3c4ff28f354aede202d54e9d1c5529a3bf87d8', + sha256: 'db06c3534964e3fc79d2763144ba53742d7fa250ca336f4a0fe724b75aaff386', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137682997939504, + timestamp_utc: '2019-09-24 03:11:39Z', + unique_pid: 21120, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137682997939504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Windows\\explorer.exe" ', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'ac4c51eb24aa95b77f705ab159189e24', + node_id: 21166, + opcode: 1, + original_file_name: 'EXPLORER.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3548, + ppid: 1808, + process_name: 'explorer.exe', + process_path: 'C:\\Windows\\explorer.exe', + serial_event_id: 21166, + sha1: '4583daf9442880204730fb2c8a060430640494b1', + sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137683166079504, + timestamp_utc: '2019-09-24 03:11:56Z', + unique_pid: 21166, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683166079504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 1, + md5: 'ac4c51eb24aa95b77f705ab159189e24', + node_id: 21173, + opcode: 2, + original_file_name: 'EXPLORER.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3548, + ppid: 1808, + process_name: 'explorer.exe', + process_path: 'C:\\Windows\\explorer.exe', + serial_event_id: 21173, + sha1: '4583daf9442880204730fb2c8a060430640494b1', + sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 21166, + timestamp: 132137683166729504, + timestamp_utc: '2019-09-24 03:11:56Z', + unique_pid: 21166, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683166729504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Python27\\python.exe" "C:\\tmp\\dns.py" ', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: '743b91619fbfee3c3e173ba5a17b1290', + node_id: 21480, + opcode: 1, + original_file_name: '', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 4060, + ppid: 1808, + process_name: 'python.exe', + process_path: 'C:\\Python27\\python.exe', + serial_event_id: 21480, + sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', + sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', + signature_signer: '', + signature_status: 'noSignature', + source_id: 18943, + timestamp: 132137683493349504, + timestamp_utc: '2019-09-24 03:12:29Z', + unique_pid: 21480, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683493349504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: '743b91619fbfee3c3e173ba5a17b1290', + node_id: 21500, + opcode: 2, + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 4060, + ppid: 1808, + process_name: 'python.exe', + process_path: 'C:\\Python27\\python.exe', + serial_event_id: 21500, + sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', + sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', + signature_status: 'noSignature', + source_id: 21480, + timestamp: 132137683493889504, + timestamp_utc: '2019-09-24 03:12:29Z', + unique_pid: 21480, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683493889504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Python27\\python.exe" "C:\\tmp\\dns.py" ', + depth: 2, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: '743b91619fbfee3c3e173ba5a17b1290', + node_id: 21539, + opcode: 1, + original_file_name: '', + parent_process_name: 'cmd.exe', + parent_process_path: 'C:\\Windows\\System32\\cmd.exe', + pid: 2888, + ppid: 3280, + process_name: 'python.exe', + process_path: 'C:\\Python27\\python.exe', + serial_event_id: 21539, + sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', + sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', + signature_signer: '', + signature_status: 'noSignature', + source_id: 21120, + timestamp: 132137683555889504, + timestamp_utc: '2019-09-24 03:12:35Z', + unique_pid: 21539, + unique_ppid: 21120, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683555889504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 3, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: '743b91619fbfee3c3e173ba5a17b1290', + node_id: 21540, + opcode: 2, + parent_process_name: 'cmd.exe', + parent_process_path: 'C:\\Windows\\System32\\cmd.exe', + pid: 2888, + ppid: 3280, + process_name: 'python.exe', + process_path: 'C:\\Python27\\python.exe', + serial_event_id: 21540, + sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', + sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', + signature_status: 'noSignature', + source_id: 21539, + timestamp: 132137683556159504, + timestamp_utc: '2019-09-24 03:12:35Z', + unique_pid: 21539, + unique_ppid: 21120, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683556159504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + depth: 2, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'c29675ce0750f73225bf05d03080dfb2', + node_id: 21634, + opcode: 1, + original_file_name: '', + parent_process_name: 'cmd.exe', + parent_process_path: 'C:\\Windows\\System32\\cmd.exe', + pid: 3996, + ppid: 3280, + process_name: 'fakenet.exe', + process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + serial_event_id: 21634, + sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', + sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', + signature_signer: '', + signature_status: 'noSignature', + source_id: 21120, + timestamp: 132137683921669504, + timestamp_utc: '2019-09-24 03:13:12Z', + unique_pid: 21634, + unique_ppid: 21120, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683921669504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + depth: 3, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'c29675ce0750f73225bf05d03080dfb2', + node_id: 21669, + opcode: 1, + original_file_name: '', + parent_process_name: 'fakenet.exe', + parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + pid: 184, + ppid: 3996, + process_name: 'fakenet.exe', + process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + serial_event_id: 21669, + sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', + sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', + signature_signer: '', + signature_status: 'noSignature', + source_id: 21634, + timestamp: 132137683923819504, + timestamp_utc: '2019-09-24 03:13:12Z', + unique_pid: 21669, + unique_ppid: 21634, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683923819504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 4, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 1, + md5: 'c29675ce0750f73225bf05d03080dfb2', + node_id: 21679, + opcode: 2, + parent_process_name: 'fakenet.exe', + parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + pid: 184, + ppid: 3996, + process_name: 'fakenet.exe', + process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + serial_event_id: 21679, + sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', + sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', + signature_status: 'noSignature', + source_id: 21669, + timestamp: 132137683931089504, + timestamp_utc: '2019-09-24 03:13:13Z', + unique_pid: 21669, + unique_ppid: 21634, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683931089504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 3, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 1, + md5: 'c29675ce0750f73225bf05d03080dfb2', + node_id: 21694, + opcode: 2, + parent_process_name: 'cmd.exe', + parent_process_path: 'C:\\Windows\\System32\\cmd.exe', + pid: 3996, + ppid: 3280, + process_name: 'fakenet.exe', + process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + serial_event_id: 21694, + sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', + sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', + signature_status: 'noSignature', + source_id: 21634, + timestamp: 132137683931569504, + timestamp_utc: '2019-09-24 03:13:13Z', + unique_pid: 21634, + unique_ppid: 21120, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137683931569504, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: + '"C:\\Windows\\system32\\NOTEPAD.EXE" C:\\tmp\\fakenet1.4.3\\configs\\default.ini', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'f2c7bb8acc97f92e987a2d4087d021b1', + node_id: 21769, + opcode: 1, + original_file_name: 'NOTEPAD.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 2492, + ppid: 1808, + process_name: 'notepad.exe', + process_path: 'C:\\Windows\\System32\\notepad.exe', + serial_event_id: 21769, + sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', + sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137684112851830, + timestamp_utc: '2019-09-24 03:13:31Z', + unique_pid: 21769, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137684112851830, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: 'f2c7bb8acc97f92e987a2d4087d021b1', + node_id: 21794, + opcode: 2, + original_file_name: 'NOTEPAD.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 2492, + ppid: 1808, + process_name: 'notepad.exe', + process_path: 'C:\\Windows\\System32\\notepad.exe', + serial_event_id: 21794, + sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', + sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 21769, + timestamp: 132137684131573702, + timestamp_utc: '2019-09-24 03:13:33Z', + unique_pid: 21769, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137684131573702, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: 'fakenet.exe', + depth: 2, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'c29675ce0750f73225bf05d03080dfb2', + node_id: 21890, + opcode: 1, + original_file_name: '', + parent_process_name: 'cmd.exe', + parent_process_path: 'C:\\Windows\\System32\\cmd.exe', + pid: 1060, + ppid: 3280, + process_name: 'fakenet.exe', + process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + serial_event_id: 21890, + sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', + sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', + signature_signer: '', + signature_status: 'noSignature', + source_id: 21120, + timestamp: 132137684579848525, + timestamp_utc: '2019-09-24 03:14:17Z', + unique_pid: 21890, + unique_ppid: 21120, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137684579848525, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: 'fakenet.exe', + depth: 3, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'c29675ce0750f73225bf05d03080dfb2', + node_id: 21924, + opcode: 1, + original_file_name: '', + parent_process_name: 'fakenet.exe', + parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + pid: 4024, + ppid: 1060, + process_name: 'fakenet.exe', + process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', + serial_event_id: 21924, + sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', + sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', + signature_signer: '', + signature_status: 'noSignature', + source_id: 21890, + timestamp: 132137684580468587, + timestamp_utc: '2019-09-24 03:14:18Z', + unique_pid: 21924, + unique_ppid: 21890, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137684580468587, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Windows\\System32\\cmd.exe" ', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: '5746bd7e255dd6a8afa06f7c42c1ba41', + node_id: 22238, + opcode: 1, + original_file_name: 'Cmd.Exe', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 3328, + ppid: 1808, + process_name: 'cmd.exe', + process_path: 'C:\\Windows\\System32\\cmd.exe', + serial_event_id: 22238, + sha1: '0f3c4ff28f354aede202d54e9d1c5529a3bf87d8', + sha256: 'db06c3534964e3fc79d2763144ba53742d7fa250ca336f4a0fe724b75aaff386', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137684944024939, + timestamp_utc: '2019-09-24 03:14:54Z', + unique_pid: 22238, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137684944024939, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + attack_references: [ + { + tactics: ['Privilege Escalation', 'Execution', 'Persistence'], + technique_id: 'T1053', + technique_name: 'Scheduled Task', + }, + ], + authentication_id: 4904488, + command_line: 'SCHTASKS /CREATE /SC MINUTE /TN "Windiws" /TR "C:\\tmp\\scheduler.bat"', + depth: 2, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: '97e0ec3d6d99e8cc2b17ef2d3760e8fc', + node_id: 22376, + opcode: 1, + original_file_name: 'sctasks.exe', + parent_process_name: 'cmd.exe', + parent_process_path: 'C:\\Windows\\System32\\cmd.exe', + pid: 2864, + ppid: 3328, + process_name: 'schtasks.exe', + process_path: 'C:\\Windows\\System32\\schtasks.exe', + serial_event_id: 22376, + sha1: 'bd9dceffbcbbc82bee5f2109bd73a57477fe1f92', + sha256: '6dce7d58ebb0d705fcb4179349c441b45e160c94e43934c5ed8fa1964e2cd031', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 22238, + timestamp: 132137685249385472, + timestamp_utc: '2019-09-24 03:15:24Z', + unique_pid: 22376, + unique_ppid: 22238, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137685249385472, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 3, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: '97e0ec3d6d99e8cc2b17ef2d3760e8fc', + node_id: 22384, + opcode: 2, + original_file_name: 'sctasks.exe', + parent_process_name: 'cmd.exe', + parent_process_path: 'C:\\Windows\\System32\\cmd.exe', + pid: 2864, + ppid: 3328, + process_name: 'schtasks.exe', + process_path: 'C:\\Windows\\System32\\schtasks.exe', + serial_event_id: 22384, + sha1: 'bd9dceffbcbbc82bee5f2109bd73a57477fe1f92', + sha256: '6dce7d58ebb0d705fcb4179349c441b45e160c94e43934c5ed8fa1964e2cd031', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 22376, + timestamp: 132137685251515685, + timestamp_utc: '2019-09-24 03:15:25Z', + unique_pid: 22376, + unique_ppid: 22238, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137685251515685, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + authentication_id: 4904488, + command_line: '"C:\\Windows\\System32\\NOTEPAD.EXE" C:\\tmp\\scheduler.bat', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'f2c7bb8acc97f92e987a2d4087d021b1', + node_id: 22448, + opcode: 1, + original_file_name: 'NOTEPAD.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 4048, + ppid: 1808, + process_name: 'notepad.exe', + process_path: 'C:\\Windows\\System32\\notepad.exe', + serial_event_id: 22448, + sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', + sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137685448755407, + timestamp_utc: '2019-09-24 03:15:44Z', + unique_pid: 22448, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137685448755407, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: 'f2c7bb8acc97f92e987a2d4087d021b1', + node_id: 22464, + opcode: 2, + original_file_name: 'NOTEPAD.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 4048, + ppid: 1808, + process_name: 'notepad.exe', + process_path: 'C:\\Windows\\System32\\notepad.exe', + serial_event_id: 22464, + sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', + sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 22448, + timestamp: 132137685516752206, + timestamp_utc: '2019-09-24 03:15:51Z', + unique_pid: 22448, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137685516752206, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + attack_references: [ + { + tactics: ['Execution'], + technique_id: 'T1085', + technique_name: 'Rundll32', + }, + ], + authentication_id: 4904488, + command_line: + '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\tmp\\XLS_no_email_Upcoming Events February 2018.xls\\cb85072e6ca66a29cb0b73659a0fe5ba2456d9ba0b52e3a4c89e86549bc6e2c7.xls', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'dd81d91ff3b0763c392422865c9ac12e', + node_id: 22799, + opcode: 1, + original_file_name: 'RUNDLL32.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 2864, + ppid: 1808, + process_name: 'rundll32.exe', + process_path: 'C:\\Windows\\System32\\rundll32.exe', + serial_event_id: 22799, + sha1: '963b55acc8c566876364716d5aafa353995812a8', + sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137686572217742, + timestamp_utc: '2019-09-24 03:17:37Z', + unique_pid: 22799, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137686572217742, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: 'dd81d91ff3b0763c392422865c9ac12e', + node_id: 22805, + opcode: 2, + original_file_name: 'RUNDLL32.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 2864, + ppid: 1808, + process_name: 'rundll32.exe', + process_path: 'C:\\Windows\\System32\\rundll32.exe', + serial_event_id: 22805, + sha1: '963b55acc8c566876364716d5aafa353995812a8', + sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 22799, + timestamp: 132137686585839104, + timestamp_utc: '2019-09-24 03:17:38Z', + unique_pid: 22799, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137686585839104, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + attack_references: [ + { + tactics: ['Execution'], + technique_id: 'T1085', + technique_name: 'Rundll32', + }, + ], + authentication_id: 4904488, + command_line: + '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\tmp\\Upcoming Defense events February 2018.eml', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'dd81d91ff3b0763c392422865c9ac12e', + node_id: 22933, + opcode: 1, + original_file_name: 'RUNDLL32.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 1864, + ppid: 1808, + process_name: 'rundll32.exe', + process_path: 'C:\\Windows\\System32\\rundll32.exe', + serial_event_id: 22933, + sha1: '963b55acc8c566876364716d5aafa353995812a8', + sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137686702740793, + timestamp_utc: '2019-09-24 03:17:50Z', + unique_pid: 22933, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137686702740793, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: 'dd81d91ff3b0763c392422865c9ac12e', + node_id: 22945, + opcode: 2, + original_file_name: 'RUNDLL32.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 1864, + ppid: 1808, + process_name: 'rundll32.exe', + process_path: 'C:\\Windows\\System32\\rundll32.exe', + serial_event_id: 22945, + sha1: '963b55acc8c566876364716d5aafa353995812a8', + sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 22933, + timestamp: 132137686718432362, + timestamp_utc: '2019-09-24 03:17:51Z', + unique_pid: 22933, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137686718432362, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + attack_references: [ + { + tactics: ['Execution'], + technique_id: 'T1085', + technique_name: 'Rundll32', + }, + ], + authentication_id: 4904488, + command_line: + '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\Users\\Administrator\\AppData\\Roaming\\Microsoft\\Windows\\SendTo\\Mail Recipient.MAPIMail', + depth: 1, + elevated: true, + elevation_type: 'default', + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + integrity_level: 'high', + md5: 'dd81d91ff3b0763c392422865c9ac12e', + node_id: 27050, + opcode: 1, + original_file_name: 'RUNDLL32.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 568, + ppid: 1808, + process_name: 'rundll32.exe', + process_path: 'C:\\Windows\\System32\\rundll32.exe', + serial_event_id: 27050, + sha1: '963b55acc8c566876364716d5aafa353995812a8', + sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 18943, + timestamp: 132137686926723189, + timestamp_utc: '2019-09-24 03:18:12Z', + unique_pid: 27050, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137686926723189, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + { + collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', + data_buffer: { + depth: 2, + event_subtype_full: 'termination_event', + event_type_full: 'process_event', + exit_code: 0, + md5: 'dd81d91ff3b0763c392422865c9ac12e', + node_id: 27053, + opcode: 2, + original_file_name: 'RUNDLL32.EXE', + parent_process_name: 'explorer.exe', + parent_process_path: 'C:\\Windows\\explorer.exe', + pid: 568, + ppid: 1808, + process_name: 'rundll32.exe', + process_path: 'C:\\Windows\\System32\\rundll32.exe', + serial_event_id: 27053, + sha1: '963b55acc8c566876364716d5aafa353995812a8', + sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', + signature_signer: 'Microsoft Windows', + signature_status: 'trusted', + source_id: 27050, + timestamp: 132137686939784495, + timestamp_utc: '2019-09-24 03:18:13Z', + unique_pid: 27050, + unique_ppid: 18943, + user_domain: 'ENDPOINT-W-1-07', + user_name: 'vagrant', + user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', + }, + event_timestamp: 132137686939784495, + event_type: 4, + machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', + serial_event_id: 0, + }, + ], + status: 'success', + task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', + total_events_searched: 7730, + type: 'eventLoggingSearchResponse', + }, + }, + metadata: { + count: 39, + next: null, + next_url: null, + per_page: '4000', + previous_url: null, + timestamp: '2019-12-18T19:31:27.565110', + }, +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts new file mode 100644 index 00000000000000..f62a37802f588f --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -0,0 +1,302 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; +import { ResolverState } from '../../types'; +import { sampleData } from './sample'; +import { depthFirstPreorder, levelOrder } from '../../lib/tree_sequencers'; +import { Vector2 } from '../../types'; +import { add as vector2Add } from '../../lib/vector2'; + +const unit = 100; +const distanceBetweenNodesInUnits = 1; +const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; + +function dataSelector(state: ResolverState) { + return sampleData.data.result.search_results; +} + +export function isGraphableProcess(event) { + return eventType(event) === 'processCreated' || eventType(event) === 'processRan'; +} + +export function eventType(event) { + const { + data_buffer: { event_type_full: type, event_subtype_full: subType }, + } = event; + + if (type === 'process_event') { + if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { + return 'processCreated'; + } else if (subType === 'already_running') { + return 'processRan'; + } else if (subType === 'termination_event') { + return 'processTerminated'; + } else { + return 'unknownProcessEvent'; + } + } else if (type === 'alert_event') { + return 'processCausedAlert'; + } + return 'unknownEvent'; +} + +function uniquePidForProcess(event) { + return event.data_buffer.node_id; +} + +function uniqueParentPidForProcess(event) { + return event.data_buffer.source_id; +} + +function yHalfWayBetweenSourceAndTarget(sourcePosition: Vector2, targetPosition: Vector2) { + return sourcePosition[1] + (targetPosition[1] - sourcePosition[1]) / 2; +} + +function childrenOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, parentProcess) { + const uniqueParentPid = uniquePidForProcess(parentProcess); + const children = graphableProcessesPidMaps.processesByUniqueParentPid.get(uniqueParentPid); + return children === undefined ? [] : children; +} + +function parentOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, childProcess) { + const uniqueParentPid = uniqueParentPidForProcess(childProcess); + return graphableProcessesPidMaps.processesByUniquePid.get(uniqueParentPid); +} + +function isProcessOnlyChildFromGraphableProcessPidMaps(graphableProcessesPidMaps, childProcess) { + const parentProcess = parentOfProcessFromGraphableProcessesPidMaps( + graphableProcessesPidMaps, + childProcess + ); + return ( + childrenOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, parentProcess) + .length === 1 + ); +} + +export function graphableProcesses(state: ResolverState) { + return dataSelector(state).filter(isGraphableProcess); +} + +const graphableProcessesPidMaps = createSelector( + graphableProcesses, + function graphableProcessesPidMaps( + /* eslint-disable no-shadow */ + graphableProcesses + /* eslint-disable no-shadow */ + ) { + const processesByUniqueParentPid = new Map(); + const processesByUniquePid = new Map(); + + for (const process of graphableProcesses) { + processesByUniquePid.set(uniquePidForProcess(process), process); + const uniqueParentPid = uniqueParentPidForProcess(process); + if (processesByUniqueParentPid.has(uniqueParentPid)) { + processesByUniqueParentPid.get(uniqueParentPid).push(process); + } else { + processesByUniqueParentPid.set(uniqueParentPid, [process]); + } + } + + return { + processesByUniqueParentPid, + processesByUniquePid, + }; + } +); +const widthOfProcessSubtrees = createSelector( + graphableProcesses, + graphableProcessesPidMaps, + function widthOfProcessSubtrees( + /* eslint-disable no-shadow */ + graphableProcesses, + graphableProcessesPidMaps + /* eslint-enable no-shadow */ + ) { + const processesInReverseLevelOrder = [ + ...levelOrder({ + root: graphableProcesses[0], + children: childrenOfProcess, + }), + ].reverse(); + + const widths = new Map(); + + for (const process of processesInReverseLevelOrder) { + const children = childrenOfProcess(process); + + const sumOfWidthOfChildren = function sumOfWidthOfChildren() { + return children.reduce(function sum(currentValue, child) { + return currentValue + widths.get(child); + }, 0); + }; + + const width = + sumOfWidthOfChildren() + Math.max(0, children.length - 1) * distanceBetweenNodes; + widths.set(process, width); + } + + return widths; + + function childrenOfProcess(parentProcess) { + return childrenOfProcessFromGraphableProcessesPidMaps( + graphableProcessesPidMaps, + parentProcess + ); + } + } +); + +export const processNodePositionsAndEdgeLineSegments = createSelector( + graphableProcesses, + graphableProcessesPidMaps, + widthOfProcessSubtrees, + function processNodePositionsAndEdgeLineSegments( + /* eslint-disable no-shadow */ + graphableProcesses, + graphableProcessesPidMaps, + widthOfProcessSubtrees + /* eslint-enable no-shadow */ + ) { + const positions = new Map(); + const edgeLineSegments = []; + let parentProcess = null; + let numberOfPrecedingSiblings = null; + let runningWidthOfPrecedingSiblings = null; + for (const process of levelOrder({ + root: graphableProcesses[0], + children: childrenOfProcess, + })) { + if (parentProcess === null) { + parentProcess = process; + numberOfPrecedingSiblings = 0; + runningWidthOfPrecedingSiblings = 0; + // const yOffset = originHasChildren + // ? distanceBetweenNodes * (lineageEvents.length + 0.5) + // : distanceBetweenNodes * lineageEvents.length + + const yOffset = distanceBetweenNodes * 1; + positions.set(process, [0, yOffset]); + } else { + if (parentProcess !== parentOfProcess(process)) { + parentProcess = parentOfProcess(process); + numberOfPrecedingSiblings = 0; + runningWidthOfPrecedingSiblings = 0; + } + + const xOffset = + widthOfProcessSubtrees.get(parentProcess) / -2 + + numberOfPrecedingSiblings * distanceBetweenNodes + + runningWidthOfPrecedingSiblings + + widthOfProcessSubtrees.get(process) / 2; + + const position = vector2Add([xOffset, -distanceBetweenNodes], positions.get(parentProcess)); + + positions.set(process, position); + + const edgeLineSegmentsForProcess = function edgeLineSegmentsForProcess() { + const parentProcessPosition = positions.get(parentProcess); + const midwayY = yHalfWayBetweenSourceAndTarget(parentProcessPosition, position); + + // If this is the first child + if (numberOfPrecedingSiblings === 0) { + if (isProcessOnlyChild(process)) { + // add a single line segment directly from parent to child + return [[parentProcessPosition, position]]; + } else { + // Draw 3 line segments + // One from the parent to the midway line, + // The midway line (a horizontal line the width of the parent, halfway between the parent and child) + // A line from the child to the midway line + return [lineFromParentToMidwayLine(), midwayLine(), lineFromProcessToMidwayLine()]; + } + } else { + // If this isn't the first child, it must have siblings (the first of which drew the midway line and line + // from the parent to the midway line + return [lineFromProcessToMidwayLine()]; + } + + function lineFromParentToMidwayLine() { + return [ + // Add a line from parent to midway point + parentProcessPosition, + [parentProcessPosition[0], midwayY], + ]; + } + + function lineFromProcessToMidwayLine() { + return [ + [ + position[0], + // Simulate a capped line by moving this up a bit so it overlaps with the midline segment + midwayY, + ], + position, + ]; + } + + function midwayLine() { + /* eslint-disable no-shadow */ + const parentProcessPosition = positions.get(parentProcess); + /* eslint-enable no-shadow */ + const childrenOfParent = childrenOfProcess(parentProcess); + const lastChild = childrenOfParent[childrenOfParent.length - 1]; + + const widthOfParent = widthOfProcessSubtrees.get(parentProcess); + const widthOfFirstChild = widthOfProcessSubtrees.get(process); + const widthOfLastChild = widthOfProcessSubtrees.get(lastChild); + const widthOfMidline = widthOfParent - widthOfFirstChild / 2 - widthOfLastChild / 2; + + const minX = widthOfParent / -2 + widthOfFirstChild / 2; + const maxX = minX + widthOfMidline; + + return [ + [ + // Position line relative to the parent's x component + parentProcessPosition[0] + minX, + midwayY, + ], + [ + // Position line relative to the parent's x component + parentProcessPosition[0] + maxX, + midwayY, + ], + ]; + } + }; + + edgeLineSegments.push(...edgeLineSegmentsForProcess()); + numberOfPrecedingSiblings += 1; + runningWidthOfPrecedingSiblings += widthOfProcessSubtrees.get(process); + } + } + + function childrenOfProcess( + /* eslint-disable no-shadow */ + parentProcess + /* eslint-enable no-shadow */ + ) { + return childrenOfProcessFromGraphableProcessesPidMaps( + graphableProcessesPidMaps, + parentProcess + ); + } + + function parentOfProcess(childProcess) { + return parentOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, childProcess); + } + + function isProcessOnlyChild(childProcess) { + return isProcessOnlyChildFromGraphableProcessPidMaps(graphableProcessesPidMaps, childProcess); + } + + return { + processNodePositions: positions, + edgeLineSegments, + }; + } +); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx new file mode 100644 index 00000000000000..8b50955c1b55f9 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { useSelector } from 'react-redux'; +import { applyMatrix3 } from '../lib/vector2'; +import { Vector2 } from '../types'; +import * as selectors from '../store/selectors'; + +export const EdgeLine = styled( + React.memo( + ({ + className, + startPosition, + endPosition, + }: { + className?: string; + startPosition: Vector2; + endPosition: Vector2; + }) => { + const projectionMatrix = useSelector(selectors.projectionMatrix); + const [left, top] = applyMatrix3(startPosition, projectionMatrix); + const length = distance(startPosition[0], startPosition[1], endPosition[0], endPosition[1]); + const deltaX = endPosition[0] - startPosition[0]; + const deltaY = endPosition[1] - startPosition[1]; + const angle = -Math.atan2(deltaY, deltaX); + /** + * https://www.mathsisfun.com/algebra/distance-2-points.html + */ + function distance(x1, y1, x2, y2) { + return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); + } + + const style = { + left: left + 'px', + top: top + 'px', + width: length + 'px', + transformOrigin: 'top left', + transform: `translateY(-50%) rotateZ(${angle}rad)`, + }; + return
; + } + ) +)` + position: absolute; + height: 3px; + background-color: #d4d4d4; + color: #333333; +`; 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 eff7995badffa0..4390f625b8088e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -13,6 +13,9 @@ import * as selectors from '../store/selectors'; import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect'; import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler'; import { DiagnosticDot } from './diagnostic_dot'; +import { ProcessEventDot } from './process_event_dot'; +import { EdgeLine } from './edge_line'; +import * as dataSelectors from '../store/data/selectors'; export const AppRoot = React.memo(({ store }: { store: Store }) => { return ( @@ -26,6 +29,10 @@ const Resolver = styled( React.memo(({ className }: { className?: string }) => { const dispatch: (action: ResolverAction) => unknown = useDispatch(); + const { processNodePositions, edgeLineSegments } = useSelector( + dataSelectors.processNodePositionsAndEdgeLineSegments + ); + const [ref, setRef] = useState(null); const userIsPanning = useSelector(selectors.userIsPanning); @@ -153,8 +160,11 @@ const Resolver = styled( ref={refCallback} onMouseDown={handleMouseDown} > - {dotPositions.map((worldPosition, index) => ( - + {Array.from(processNodePositions).map(([processEvent, position], index) => ( + + ))} + {edgeLineSegments.map(([startPosition, endPosition], index) => ( + ))}
); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx new file mode 100644 index 00000000000000..75f900a4e0e1c5 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { useSelector } from 'react-redux'; +import { applyMatrix3 } from '../lib/vector2'; +import { Vector2 } from '../types'; +import * as selectors from '../store/selectors'; + +export const ProcessEventDot = styled( + React.memo( + ({ + className, + worldPosition, + processEvent, + }: { + className?: string; + worldPosition: Vector2; + processEvent: any; + }) => { + const projectionMatrix = useSelector(selectors.projectionMatrix); + const [left, top] = applyMatrix3(worldPosition, projectionMatrix); + const style = { + left: (left - 20).toString() + 'px', + top: (top - 20).toString() + 'px', + }; + return ( + + name: {processEvent.data_buffer.process_name} +
+ 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; +`; From 6ed311034923806e38a36605ee1bed634a94c0c9 Mon Sep 17 00:00:00 2001 From: Pedro Jaramillo Date: Wed, 8 Jan 2020 11:05:43 -0500 Subject: [PATCH 58/86] Add reducer, action, tests, and refactor --- .../public/embeddables/resolver/actions.ts | 3 +- .../resolver/lib/tree_sequencers.ts | 11 +- .../resolver/models/process_event.test.ts | 24 +++ .../resolver/models/process_event.ts | 40 ++++ .../embeddables/resolver/store/data/action.ts | 12 ++ .../resolver/store/data/graphing.test.ts | 174 ++++++++++++++++++ .../embeddables/resolver/store/data/index.ts | 8 + .../resolver/store/data/reducer.ts | 31 ++++ .../resolver/store/data/selectors.ts | 104 ++++------- .../embeddables/resolver/store/reducer.ts | 2 + .../embeddables/resolver/store/selectors.ts | 13 ++ .../public/embeddables/resolver/types.ts | 37 ++++ .../embeddables/resolver/view/index.tsx | 3 +- .../resolver/view/process_event_dot.tsx | 4 +- 14 files changed, 387 insertions(+), 79 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/data/index.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts index a7297b05d002ca..c7f790588a739e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { CameraAction } from './store/camera'; +import { DataAction } from './store/data'; -export type ResolverAction = CameraAction; +export type ResolverAction = CameraAction | DataAction; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts index 1444e49a4d4672..09a056f7a32bd3 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -// TODO: type root and children -export function* depthFirstPreorder({ root, children }) { +export function* depthFirstPreorder(root: T, children: (parent: T) => T[]): Iterable { const nodesToVisit = [root]; while (nodesToVisit.length !== 0) { const currentNode = nodesToVisit.shift(); - nodesToVisit.unshift(...(children(currentNode) || [])); - yield currentNode; + if (currentNode !== undefined) { + nodesToVisit.unshift(...(children(currentNode) || [])); + yield currentNode; + } } } -export function* levelOrder({ root, children }) { +export function* levelOrder(root: T, children: (parent: T) => T[]): Iterable { let level = [root]; while (level.length !== 0) { let nextLevel = []; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts new file mode 100644 index 00000000000000..201de4032564b8 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { eventType } from './process_event'; +import { ProcessEvent } from '../types'; + +describe('process event', () => { + describe('eventType', () => { + let event: ProcessEvent; + beforeEach(() => { + event = { + data_buffer: { + event_type_full: 'process_event', + }, + }; + }); + it("returns the right value when the subType is 'creation_event'", () => { + event.data_buffer.event_subtype_full = 'creation_event'; + expect(eventType(event)).toEqual('processCreated'); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts new file mode 100644 index 00000000000000..5dcf7ece8808d6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ProcessEvent } from '../types'; + +export function isGraphableProcess(event: ProcessEvent) { + return eventType(event) === 'processCreated' || eventType(event) === 'processRan'; +} + +export function eventType(event: ProcessEvent) { + const { + data_buffer: { event_type_full: type, event_subtype_full: subType }, + } = event; + + if (type === 'process_event') { + if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { + return 'processCreated'; + } else if (subType === 'already_running') { + return 'processRan'; + } else if (subType === 'termination_event') { + return 'processTerminated'; + } else { + return 'unknownProcessEvent'; + } + } else if (type === 'alert_event') { + return 'processCausedAlert'; + } + return 'unknownEvent'; +} + +export function uniquePidForProcess(event: ProcessEvent) { + return event.data_buffer.node_id; +} + +export function uniqueParentPidForProcess(event: ProcessEvent) { + return event.data_buffer.source_id; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts new file mode 100644 index 00000000000000..980f47f0823e76 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface ServerReturnedResolverData { + readonly type: 'serverReturnedResolverData'; + readonly payload: Record; +} + +export type DataAction = ServerReturnedResolverData; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts new file mode 100644 index 00000000000000..b3af297fc49f35 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Store, createStore, AnyAction } from 'redux'; +import { DataAction } from './action'; +import { dataReducer } from './reducer'; +import { DataState, Vector2 } from '../../types'; +import { + graphableProcesses, + widthOfProcessSubtrees, + distanceBetweenNodes, + processNodePositionsAndEdgeLineSegments, +} from './selectors'; + +describe('resolver graph layout', () => { + let store: Store; + + beforeEach(() => { + store = createStore(dataReducer, undefined); + }); + describe('resolver data is received', () => { + /* + * A + * ____|____ + * | | + * B C + * ___|___ ___|___ + * | | | | + * D E F G + * | + * H + * + */ + const processA = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 0, + }, + }; + const processB = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'already_running', + node_id: 1, + source_id: 0, + }, + }; + const processC = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 2, + source_id: 0, + }, + }; + const processD = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 3, + source_id: 1, + }, + }; + const processE = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 4, + source_id: 1, + }, + }; + const processF = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 5, + source_id: 2, + }, + }; + const processG = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 6, + source_id: 2, + }, + }; + const processH = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 7, + source_id: 6, + }, + }; + const processI = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'termination_event', + node_id: 8, + source_id: 0, + }, + }; + beforeEach(() => { + const payload = { + data: { + result: { + search_results: [ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + processH, + processI, + ], + }, + }, + }; + const action: DataAction = { type: 'serverReturnedResolverData', payload }; + store.dispatch(action); + }); + it("the graphableProcesses list should only include events with 'processCreated' an 'processRan' eventType", () => { + const actual = graphableProcesses(store.getState()); + expect(actual).toEqual([ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + processH, + ]); + }); + it('the width of process subtress is calculated correctly', () => { + const expected = new Map([ + [processA, 3 * distanceBetweenNodes], + [processB, 1 * distanceBetweenNodes], + [processC, 1 * distanceBetweenNodes], + [processD, 0 * distanceBetweenNodes], + [processE, 0 * distanceBetweenNodes], + [processF, 0 * distanceBetweenNodes], + [processG, 0 * distanceBetweenNodes], + [processH, 0 * distanceBetweenNodes], + ]); + const actual = widthOfProcessSubtrees(store.getState()); + expect(actual).toEqual(expected); + }); + it('it renders the nodes at the right positions', () => { + const expected = new Map([ + [processA, [0, 100]], + [processB, [-100, 0]], + [processC, [100, 0]], + [processD, [-150, -100]], + [processE, [-50, -100]], + [processF, [50, -100]], + [processG, [150, -100]], + [processH, [150, -200]], + ]); + const actual = processNodePositionsAndEdgeLineSegments(store.getState()).processNodePositions; + expect(actual).toEqual(expected); + }); + it('it renders edges at the right positions', () => { + expect(false).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/index.ts new file mode 100644 index 00000000000000..8db57c5d9681f4 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { dataReducer } from './reducer'; +export { DataAction } from './action'; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts new file mode 100644 index 00000000000000..848d814808bac4 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Reducer } from 'redux'; +import { DataState, ResolverAction } from '../../types'; +import { sampleData } from './sample'; + +function initialState(): DataState { + return { + results: sampleData.data.result.search_results, + }; +} + +export const dataReducer: Reducer = (state = initialState(), action) => { + if (action.type === 'serverReturnedResolverData') { + const { + data: { + result: { search_results }, + }, + } = action.payload; + return { + ...state, + results: search_results, + }; + } else { + return state; + } +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index f62a37802f588f..4b37ef58bd8e72 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -5,69 +5,45 @@ */ import { createSelector } from 'reselect'; -import { ResolverState } from '../../types'; -import { sampleData } from './sample'; -import { depthFirstPreorder, levelOrder } from '../../lib/tree_sequencers'; +import { DataState, ProcessEvent, GraphableProcessesPidMaps } from '../../types'; +import { levelOrder } from '../../lib/tree_sequencers'; import { Vector2 } from '../../types'; import { add as vector2Add } from '../../lib/vector2'; +import { + isGraphableProcess, + uniquePidForProcess, + uniqueParentPidForProcess, +} from '../../models/process_event'; const unit = 100; const distanceBetweenNodesInUnits = 1; -const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; - -function dataSelector(state: ResolverState) { - return sampleData.data.result.search_results; -} - -export function isGraphableProcess(event) { - return eventType(event) === 'processCreated' || eventType(event) === 'processRan'; -} - -export function eventType(event) { - const { - data_buffer: { event_type_full: type, event_subtype_full: subType }, - } = event; - - if (type === 'process_event') { - if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { - return 'processCreated'; - } else if (subType === 'already_running') { - return 'processRan'; - } else if (subType === 'termination_event') { - return 'processTerminated'; - } else { - return 'unknownProcessEvent'; - } - } else if (type === 'alert_event') { - return 'processCausedAlert'; - } - return 'unknownEvent'; -} - -function uniquePidForProcess(event) { - return event.data_buffer.node_id; -} - -function uniqueParentPidForProcess(event) { - return event.data_buffer.source_id; -} +export const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; function yHalfWayBetweenSourceAndTarget(sourcePosition: Vector2, targetPosition: Vector2) { return sourcePosition[1] + (targetPosition[1] - sourcePosition[1]) / 2; } -function childrenOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, parentProcess) { +function childrenOfProcessFromGraphableProcessesPidMaps( + graphableProcessesPidMaps: GraphableProcessesPidMaps, + parentProcess: ProcessEvent +) { const uniqueParentPid = uniquePidForProcess(parentProcess); const children = graphableProcessesPidMaps.processesByUniqueParentPid.get(uniqueParentPid); return children === undefined ? [] : children; } -function parentOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, childProcess) { +function parentOfProcessFromGraphableProcessesPidMaps( + graphableProcessesPidMaps: GraphableProcessesPidMaps, + childProcess: ProcessEvent +) { const uniqueParentPid = uniqueParentPidForProcess(childProcess); return graphableProcessesPidMaps.processesByUniquePid.get(uniqueParentPid); } -function isProcessOnlyChildFromGraphableProcessPidMaps(graphableProcessesPidMaps, childProcess) { +function isProcessOnlyChildFromGraphableProcessPidMaps( + graphableProcessesPidMaps: GraphableProcessesPidMaps, + childProcess: ProcessEvent +) { const parentProcess = parentOfProcessFromGraphableProcessesPidMaps( graphableProcessesPidMaps, childProcess @@ -78,8 +54,8 @@ function isProcessOnlyChildFromGraphableProcessPidMaps(graphableProcessesPidMaps ); } -export function graphableProcesses(state: ResolverState) { - return dataSelector(state).filter(isGraphableProcess); +export function graphableProcesses(state: DataState) { + return state.results.filter(isGraphableProcess); } const graphableProcessesPidMaps = createSelector( @@ -88,9 +64,9 @@ const graphableProcessesPidMaps = createSelector( /* eslint-disable no-shadow */ graphableProcesses /* eslint-disable no-shadow */ - ) { - const processesByUniqueParentPid = new Map(); - const processesByUniquePid = new Map(); + ): GraphableProcessesPidMaps { + const processesByUniqueParentPid = new Map(); + const processesByUniquePid = new Map(); for (const process of graphableProcesses) { processesByUniquePid.set(uniquePidForProcess(process), process); @@ -108,7 +84,7 @@ const graphableProcessesPidMaps = createSelector( }; } ); -const widthOfProcessSubtrees = createSelector( +export const widthOfProcessSubtrees = createSelector( graphableProcesses, graphableProcessesPidMaps, function widthOfProcessSubtrees( @@ -118,10 +94,7 @@ const widthOfProcessSubtrees = createSelector( /* eslint-enable no-shadow */ ) { const processesInReverseLevelOrder = [ - ...levelOrder({ - root: graphableProcesses[0], - children: childrenOfProcess, - }), + ...levelOrder(graphableProcesses[0], childrenOfProcess), ].reverse(); const widths = new Map(); @@ -142,7 +115,7 @@ const widthOfProcessSubtrees = createSelector( return widths; - function childrenOfProcess(parentProcess) { + function childrenOfProcess(parentProcess: ProcessEvent) { return childrenOfProcessFromGraphableProcessesPidMaps( graphableProcessesPidMaps, parentProcess @@ -164,21 +137,14 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( ) { const positions = new Map(); const edgeLineSegments = []; - let parentProcess = null; - let numberOfPrecedingSiblings = null; - let runningWidthOfPrecedingSiblings = null; - for (const process of levelOrder({ - root: graphableProcesses[0], - children: childrenOfProcess, - })) { + let parentProcess: ProcessEvent = null; + let numberOfPrecedingSiblings = 0; + let runningWidthOfPrecedingSiblings = 0; + for (const process of levelOrder(graphableProcesses[0], childrenOfProcess)) { if (parentProcess === null) { parentProcess = process; numberOfPrecedingSiblings = 0; runningWidthOfPrecedingSiblings = 0; - // const yOffset = originHasChildren - // ? distanceBetweenNodes * (lineageEvents.length + 0.5) - // : distanceBetweenNodes * lineageEvents.length - const yOffset = distanceBetweenNodes * 1; positions.set(process, [0, yOffset]); } else { @@ -277,7 +243,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( function childrenOfProcess( /* eslint-disable no-shadow */ - parentProcess + parentProcess: ProcessEvent /* eslint-enable no-shadow */ ) { return childrenOfProcessFromGraphableProcessesPidMaps( @@ -286,11 +252,11 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( ); } - function parentOfProcess(childProcess) { + function parentOfProcess(childProcess: ProcessEvent) { return parentOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, childProcess); } - function isProcessOnlyChild(childProcess) { + function isProcessOnlyChild(childProcess: ProcessEvent) { return isProcessOnlyChildFromGraphableProcessPidMaps(graphableProcessesPidMaps, childProcess); } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts index 5f7d140a0bed57..97ab51cbd6deaa 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts @@ -5,8 +5,10 @@ */ import { Reducer, combineReducers } from 'redux'; import { cameraReducer } from './camera/reducer'; +import { dataReducer } from './data/reducer'; import { ResolverState, ResolverAction } from '../types'; export const resolverReducer: Reducer = combineReducers({ camera: cameraReducer, + data: dataReducer, }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 402e0ec8ca0d9d..30adf172030969 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -5,6 +5,7 @@ */ import * as cameraSelectors from './camera/selectors'; +import * as dataSelectors from './data/selectors'; import { ResolverState } from '../types'; /** @@ -35,6 +36,11 @@ export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale */ export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelectors.userIsPanning); +export const processNodePositionsAndEdgeLineSegments = composeSelectors( + dataStateSelector, + dataSelectors.processNodePositionsAndEdgeLineSegments +); + /** * Returns the camera state from within ResolverState */ @@ -42,6 +48,13 @@ function cameraStateSelector(state: ResolverState) { return state.camera; } +/** + * Returns the data state from within ResolverState + */ +function dataStateSelector(state: ResolverState) { + return state.data; +} + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 225a592f3c9827..ff062443545480 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -14,6 +14,11 @@ export interface ResolverState { * Contains the state of the camera. This includes panning interactions, transform, and projection. */ readonly camera: CameraState; + + /** + * Contains the state associated with event data (process events and possibly other event types). + */ + readonly data: DataState; } interface PanningState { @@ -59,6 +64,10 @@ export interface CameraState { readonly latestFocusedWorldCoordinates: Vector2 | null; } +export interface DataState { + readonly results: ProcessEvent[]; +} + export type Vector2 = readonly [number, number]; export type Vector3 = readonly [number, number, number]; @@ -91,3 +100,31 @@ export type Matrix3 = readonly [ number, number ]; + +type eventSubtypeFull = + | 'creation_event' + | 'fork_event' + | 'exec_event' + | 'already_running' + | 'termination_event'; + +type eventTypeFull = 'process_event' | 'alert_event'; + +export interface ProcessEvent { + readonly event_timestamp: number; + readonly event_type: number; + readonly machine_id: string; + readonly data_buffer: { + event_subtype_full: eventSubtypeFull; + event_type_full: eventTypeFull; + node_id: number; + source_id?: number; + process_name: string; + process_path: string; + }; +} + +export interface GraphableProcessesPidMaps { + processesByUniqueParentPid: Map; + processesByUniquePid: Map; +} 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 4390f625b8088e..f0a02459450ff8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -15,7 +15,6 @@ import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler'; import { DiagnosticDot } from './diagnostic_dot'; import { ProcessEventDot } from './process_event_dot'; import { EdgeLine } from './edge_line'; -import * as dataSelectors from '../store/data/selectors'; export const AppRoot = React.memo(({ store }: { store: Store }) => { return ( @@ -30,7 +29,7 @@ const Resolver = styled( const dispatch: (action: ResolverAction) => unknown = useDispatch(); const { processNodePositions, edgeLineSegments } = useSelector( - dataSelectors.processNodePositionsAndEdgeLineSegments + selectors.processNodePositionsAndEdgeLineSegments ); const [ref, setRef] = useState(null); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 75f900a4e0e1c5..1d7edf429f911a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -8,7 +8,7 @@ import React from 'react'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2 } from '../types'; +import { Vector2, ProcessEvent } from '../types'; import * as selectors from '../store/selectors'; export const ProcessEventDot = styled( @@ -20,7 +20,7 @@ export const ProcessEventDot = styled( }: { className?: string; worldPosition: Vector2; - processEvent: any; + processEvent: ProcessEvent; }) => { const projectionMatrix = useSelector(selectors.projectionMatrix); const [left, top] = applyMatrix3(worldPosition, projectionMatrix); From df21333dcfd7e8a47cae84fdbfe4efee6a676e76 Mon Sep 17 00:00:00 2001 From: Pedro Jaramillo Date: Wed, 8 Jan 2020 12:09:41 -0500 Subject: [PATCH 59/86] add some comments, move test variables --- .../resolver/lib/tree_sequencers.ts | 6 + .../resolver/store/data/graphing.test.ts | 179 ++++++++++-------- 2 files changed, 101 insertions(+), 84 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts index 09a056f7a32bd3..219c58d6efc9c5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * Sequences a tree, yielding children returned by the `children` function. Sequencing is done in 'depth first preorder' fashion. See https://en.wikipedia.org/wiki/Tree_traversal#Pre-order_(NLR) + */ export function* depthFirstPreorder(root: T, children: (parent: T) => T[]): Iterable { const nodesToVisit = [root]; while (nodesToVisit.length !== 0) { @@ -15,6 +18,9 @@ export function* depthFirstPreorder(root: T, children: (parent: T) => T[]): I } } +/** + * Sequences a tree, yielding children returned by the `children` function. Sequencing is done in 'level order' fashion. + */ export function* levelOrder(root: T, children: (parent: T) => T[]): Iterable { let level = [root]; while (level.length !== 0) { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index b3af297fc49f35..60339557b6efe9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -16,96 +16,107 @@ import { } from './selectors'; describe('resolver graph layout', () => { - let store: Store; + let store: Store; beforeEach(() => { store = createStore(dataReducer, undefined); }); describe('resolver data is received', () => { - /* - * A - * ____|____ - * | | - * B C - * ___|___ ___|___ - * | | | | - * D E F G - * | - * H - * - */ - const processA = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 0, - }, - }; - const processB = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'already_running', - node_id: 1, - source_id: 0, - }, - }; - const processC = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 2, - source_id: 0, - }, - }; - const processD = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 3, - source_id: 1, - }, - }; - const processE = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 4, - source_id: 1, - }, - }; - const processF = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 5, - source_id: 2, - }, - }; - const processG = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 6, - source_id: 2, - }, - }; - const processH = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 7, - source_id: 6, - }, - }; - const processI = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'termination_event', - node_id: 8, - source_id: 0, - }, - }; + let processA; + let processB; + let processC; + let processD; + let processE; + let processF; + let processG; + let processH; + let processI; + beforeEach(() => { + /* + * A + * ____|____ + * | | + * B C + * ___|___ ___|___ + * | | | | + * D E F G + * | + * H + * + */ + processA = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 0, + }, + }; + processB = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'already_running', + node_id: 1, + source_id: 0, + }, + }; + processC = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 2, + source_id: 0, + }, + }; + processD = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 3, + source_id: 1, + }, + }; + processE = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 4, + source_id: 1, + }, + }; + processF = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 5, + source_id: 2, + }, + }; + processG = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 6, + source_id: 2, + }, + }; + processH = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 7, + source_id: 6, + }, + }; + processI = { + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'termination_event', + node_id: 8, + source_id: 0, + }, + }; + const payload = { data: { result: { From 827bba80a48dca291a235db5d63e795dd030ba71 Mon Sep 17 00:00:00 2001 From: Pedro Jaramillo Date: Wed, 8 Jan 2020 12:59:15 -0500 Subject: [PATCH 60/86] small changes --- .../resolver/store/data/selectors.ts | 44 ++++++++++++------- .../public/embeddables/resolver/types.ts | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 4b37ef58bd8e72..6cee7d640bb74c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -35,9 +35,13 @@ function childrenOfProcessFromGraphableProcessesPidMaps( function parentOfProcessFromGraphableProcessesPidMaps( graphableProcessesPidMaps: GraphableProcessesPidMaps, childProcess: ProcessEvent -) { +): ProcessEvent | undefined { const uniqueParentPid = uniqueParentPidForProcess(childProcess); - return graphableProcessesPidMaps.processesByUniquePid.get(uniqueParentPid); + if (uniqueParentPid === undefined) { + return undefined; + } else { + return graphableProcessesPidMaps.processesByUniquePid.get(uniqueParentPid); + } } function isProcessOnlyChildFromGraphableProcessPidMaps( @@ -48,10 +52,15 @@ function isProcessOnlyChildFromGraphableProcessPidMaps( graphableProcessesPidMaps, childProcess ); - return ( - childrenOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, parentProcess) - .length === 1 - ); + if (parentProcess === undefined) { + // if parent process is undefined, then the child is the root. We choose not to support multiple roots + return true; + } else { + return ( + childrenOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, parentProcess) + .length === 1 + ); + } } export function graphableProcesses(state: DataState) { @@ -71,8 +80,9 @@ const graphableProcessesPidMaps = createSelector( for (const process of graphableProcesses) { processesByUniquePid.set(uniquePidForProcess(process), process); const uniqueParentPid = uniqueParentPidForProcess(process); - if (processesByUniqueParentPid.has(uniqueParentPid)) { - processesByUniqueParentPid.get(uniqueParentPid).push(process); + const processes = processesByUniqueParentPid.get(uniqueParentPid); + if (processes) { + processes.push(process); } else { processesByUniqueParentPid.set(uniqueParentPid, [process]); } @@ -137,11 +147,11 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( ) { const positions = new Map(); const edgeLineSegments = []; - let parentProcess: ProcessEvent = null; + let parentProcess: ProcessEvent | undefined; let numberOfPrecedingSiblings = 0; let runningWidthOfPrecedingSiblings = 0; for (const process of levelOrder(graphableProcesses[0], childrenOfProcess)) { - if (parentProcess === null) { + if (parentProcess === undefined) { parentProcess = process; numberOfPrecedingSiblings = 0; runningWidthOfPrecedingSiblings = 0; @@ -178,7 +188,11 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( // One from the parent to the midway line, // The midway line (a horizontal line the width of the parent, halfway between the parent and child) // A line from the child to the midway line - return [lineFromParentToMidwayLine(), midwayLine(), lineFromProcessToMidwayLine()]; + return [ + lineFromParentToMidwayLine(), + midwayLine(parentProcess), + lineFromProcessToMidwayLine(), + ]; } } else { // If this isn't the first child, it must have siblings (the first of which drew the midway line and line @@ -205,14 +219,14 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( ]; } - function midwayLine() { + function midwayLine(parentProcessNode: ProcessEvent) { /* eslint-disable no-shadow */ - const parentProcessPosition = positions.get(parentProcess); + const parentProcessPosition = positions.get(parentProcessNode); /* eslint-enable no-shadow */ - const childrenOfParent = childrenOfProcess(parentProcess); + const childrenOfParent = childrenOfProcess(parentProcessNode); const lastChild = childrenOfParent[childrenOfParent.length - 1]; - const widthOfParent = widthOfProcessSubtrees.get(parentProcess); + const widthOfParent = widthOfProcessSubtrees.get(parentProcessNode); const widthOfFirstChild = widthOfProcessSubtrees.get(process); const widthOfLastChild = widthOfProcessSubtrees.get(lastChild); const widthOfMidline = widthOfParent - widthOfFirstChild / 2 - widthOfLastChild / 2; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index ff062443545480..b03dc91a0bcf38 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -108,7 +108,7 @@ type eventSubtypeFull = | 'already_running' | 'termination_event'; -type eventTypeFull = 'process_event' | 'alert_event'; +type eventTypeFull = 'process_event'; export interface ProcessEvent { readonly event_timestamp: number; From 8340ccde2952ba7a1a0cb47b9a566ad0bc6be690 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 8 Jan 2020 13:23:35 -0500 Subject: [PATCH 61/86] fix errors in sample data --- .../embeddables/resolver/store/data/action.ts | 1 + .../embeddables/resolver/store/data/sample.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index 980f47f0823e76..a19d5654bcb726 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -6,6 +6,7 @@ interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; + // TODO how dare you readonly payload: Record; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts index 92ada11bc3f7c8..b0ed9f3554c9bb 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts @@ -4,7 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -export const sampleData = { +import { ProcessEvent } from '../../types'; + +interface ProcessEventSampleData { + data: { + result: { + search_results: ProcessEvent[]; + }; + }; +} + +const rawData = { data: { code: 200, result: { @@ -1594,3 +1604,5 @@ export const sampleData = { timestamp: '2019-12-18T19:31:27.565110', }, }; + +export const sampleData: ProcessEventSampleData = rawData as ProcessEventSampleData; From 5980c39f4c8733941427fee394aa46be15a282f5 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 8 Jan 2020 15:20:39 -0500 Subject: [PATCH 62/86] cleanup and add more tests --- .../embeddables/resolver/lib/vector2.ts | 18 + .../data/__snapshots__/graphing.test.ts.snap | 347 ++++++++++++++++++ .../resolver/store/data/graphing.test.ts | 303 ++++++++------- .../resolver/store/data/selectors.ts | 207 ++++++----- .../resolver/view/diagnostic_dot.tsx | 42 --- .../embeddables/resolver/view/edge_line.tsx | 22 +- .../embeddables/resolver/view/index.tsx | 18 +- 7 files changed, 661 insertions(+), 296 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap delete mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts index b39672682bcda7..6009b6879a6312 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts @@ -32,3 +32,21 @@ export function divide(a: Vector2, b: Vector2): Vector2 { export function applyMatrix3([x, y]: Vector2, [m11, m12, m13, m21, m22, m23]: Matrix3): Vector2 { return [x * m11 + y * m12 + m13, y * m21 + y * m22 + m23]; } + +/** + * Returns the distance between two vectors + */ +export function distance(a: Vector2, b: Vector2) { + const [x1, y1] = a; + const [x2, y2] = b; + return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); +} + +/** + * Returns the angle between two vectors + */ +export function angle(a: Vector2, b: Vector2) { + const deltaX = b[0] - a[0]; + const deltaY = b[1] - a[1]; + return Math.atan2(deltaY, deltaX); +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap new file mode 100644 index 00000000000000..40b0b8715cfa53 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -0,0 +1,347 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resolver graph layout when rendering no nodes renders right 1`] = ` +Object { + "edgeLineSegments": Array [], + "processNodePositions": Map {}, +} +`; + +exports[`resolver graph layout when rendering one node renders right 1`] = ` +Object { + "edgeLineSegments": Array [], + "processNodePositions": Map { + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 0, + "process_name": "", + "process_path": "", + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + 0, + 0, + ], + }, +} +`; + +exports[`resolver graph layout when rendering two forks, and one fork has an extra long tine renders right 1`] = ` +Object { + "edgeLineSegments": Array [ + Array [ + Array [ + 0, + 0, + ], + Array [ + 0, + -50, + ], + ], + Array [ + Array [ + -100, + -50, + ], + Array [ + 100, + -50, + ], + ], + Array [ + Array [ + -100, + -50, + ], + Array [ + -100, + -100, + ], + ], + Array [ + Array [ + 100, + -50, + ], + Array [ + 100, + -100, + ], + ], + Array [ + Array [ + -100, + -100, + ], + Array [ + -100, + -150, + ], + ], + Array [ + Array [ + -150, + -150, + ], + Array [ + -50, + -150, + ], + ], + Array [ + Array [ + -150, + -150, + ], + Array [ + -150, + -200, + ], + ], + Array [ + Array [ + -50, + -150, + ], + Array [ + -50, + -200, + ], + ], + Array [ + Array [ + 100, + -100, + ], + Array [ + 100, + -150, + ], + ], + Array [ + Array [ + 50, + -150, + ], + Array [ + 150, + -150, + ], + ], + Array [ + Array [ + 50, + -150, + ], + Array [ + 50, + -200, + ], + ], + Array [ + Array [ + 150, + -150, + ], + Array [ + 150, + -200, + ], + ], + Array [ + Array [ + 150, + -200, + ], + Array [ + 150, + -300, + ], + ], + ], + "processNodePositions": Map { + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 0, + "process_name": "", + "process_path": "", + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + 0, + 0, + ], + Object { + "data_buffer": Object { + "event_subtype_full": "already_running", + "event_type_full": "process_event", + "node_id": 1, + "process_name": "", + "process_path": "", + "source_id": 0, + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + -100, + -100, + ], + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 2, + "process_name": "", + "process_path": "", + "source_id": 0, + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + 100, + -100, + ], + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 3, + "process_name": "", + "process_path": "", + "source_id": 1, + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + -150, + -200, + ], + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 4, + "process_name": "", + "process_path": "", + "source_id": 1, + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + -50, + -200, + ], + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 5, + "process_name": "", + "process_path": "", + "source_id": 2, + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + 50, + -200, + ], + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 6, + "process_name": "", + "process_path": "", + "source_id": 2, + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + 150, + -200, + ], + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 7, + "process_name": "", + "process_path": "", + "source_id": 6, + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + 150, + -300, + ], + }, +} +`; + +exports[`resolver graph layout when rendering two nodes, one being the parent of the other renders right 1`] = ` +Object { + "edgeLineSegments": Array [ + Array [ + Array [ + 0, + 0, + ], + Array [ + 0, + -100, + ], + ], + ], + "processNodePositions": Map { + Object { + "data_buffer": Object { + "event_subtype_full": "creation_event", + "event_type_full": "process_event", + "node_id": 0, + "process_name": "", + "process_path": "", + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + 0, + 0, + ], + Object { + "data_buffer": Object { + "event_subtype_full": "already_running", + "event_type_full": "process_event", + "node_id": 1, + "process_name": "", + "process_path": "", + "source_id": 0, + }, + "event_timestamp": 1, + "event_type": 1, + "machine_id": "", + } => Array [ + 0, + -100, + ], + }, +} +`; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index 60339557b6efe9..d446cc77aaefda 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -4,119 +4,196 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Store, createStore, AnyAction } from 'redux'; +import { Store, createStore } from 'redux'; import { DataAction } from './action'; import { dataReducer } from './reducer'; -import { DataState, Vector2 } from '../../types'; -import { - graphableProcesses, - widthOfProcessSubtrees, - distanceBetweenNodes, - processNodePositionsAndEdgeLineSegments, -} from './selectors'; +import { DataState, ProcessEvent } from '../../types'; +import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; + +type DeepPartial = { [K in keyof T]?: DeepPartial }; + +function processEvent( + parts: { + data_buffer: { node_id: ProcessEvent['data_buffer']['node_id'] }; + } & DeepPartial +): ProcessEvent { + const { data_buffer: dataBuffer } = parts; + return { + event_timestamp: 1, + event_type: 1, + machine_id: '', + ...parts, + data_buffer: { + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + process_name: '', + process_path: '', + ...dataBuffer, + }, + }; +} describe('resolver graph layout', () => { + let processA: ProcessEvent; + let processB: ProcessEvent; + let processC: ProcessEvent; + let processD: ProcessEvent; + let processE: ProcessEvent; + let processF: ProcessEvent; + let processG: ProcessEvent; + let processH: ProcessEvent; + let processI: ProcessEvent; let store: Store; beforeEach(() => { + /* + * A + * ____|____ + * | | + * B C + * ___|___ ___|___ + * | | | | + * D E F G + * | + * H + * + */ + processA = processEvent({ + data_buffer: { + process_name: '', + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 0, + }, + }); + processB = processEvent({ + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'already_running', + node_id: 1, + source_id: 0, + }, + }); + processC = processEvent({ + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 2, + source_id: 0, + }, + }); + processD = processEvent({ + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 3, + source_id: 1, + }, + }); + processE = processEvent({ + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 4, + source_id: 1, + }, + }); + processF = processEvent({ + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 5, + source_id: 2, + }, + }); + processG = processEvent({ + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 6, + source_id: 2, + }, + }); + processH = processEvent({ + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + node_id: 7, + source_id: 6, + }, + }); + processI = processEvent({ + data_buffer: { + event_type_full: 'process_event', + event_subtype_full: 'termination_event', + node_id: 8, + source_id: 0, + }, + }); store = createStore(dataReducer, undefined); }); - describe('resolver data is received', () => { - let processA; - let processB; - let processC; - let processD; - let processE; - let processF; - let processG; - let processH; - let processI; - + describe('when rendering no nodes', () => { beforeEach(() => { - /* - * A - * ____|____ - * | | - * B C - * ___|___ ___|___ - * | | | | - * D E F G - * | - * H - * - */ - processA = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 0, - }, - }; - processB = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'already_running', - node_id: 1, - source_id: 0, - }, - }; - processC = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 2, - source_id: 0, - }, - }; - processD = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 3, - source_id: 1, - }, - }; - processE = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 4, - source_id: 1, - }, - }; - processF = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 5, - source_id: 2, - }, - }; - processG = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 6, - source_id: 2, + const payload = { + data: { + result: { + search_results: [], + }, }, }; - processH = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - node_id: 7, - source_id: 6, + const action: DataAction = { type: 'serverReturnedResolverData', payload }; + store.dispatch(action); + }); + it('the graphableProcesses list should only include nothing', () => { + const actual = graphableProcesses(store.getState()); + expect(actual).toEqual([]); + }); + it('renders right', () => { + expect(processNodePositionsAndEdgeLineSegments(store.getState())).toMatchSnapshot(); + }); + }); + describe('when rendering one node', () => { + beforeEach(() => { + const payload = { + data: { + result: { + search_results: [processA], + }, }, }; - processI = { - data_buffer: { - event_type_full: 'process_event', - event_subtype_full: 'termination_event', - node_id: 8, - source_id: 0, + const action: DataAction = { type: 'serverReturnedResolverData', payload }; + store.dispatch(action); + }); + it('the graphableProcesses list should only include nothing', () => { + const actual = graphableProcesses(store.getState()); + expect(actual).toEqual([processA]); + }); + it('renders right', () => { + expect(processNodePositionsAndEdgeLineSegments(store.getState())).toMatchSnapshot(); + }); + }); + describe('when rendering two nodes, one being the parent of the other', () => { + beforeEach(() => { + const payload = { + data: { + result: { + search_results: [processA, processB], + }, }, }; - + const action: DataAction = { type: 'serverReturnedResolverData', payload }; + store.dispatch(action); + }); + it('the graphableProcesses list should only include nothing', () => { + const actual = graphableProcesses(store.getState()); + expect(actual).toEqual([processA, processB]); + }); + it('renders right', () => { + expect(processNodePositionsAndEdgeLineSegments(store.getState())).toMatchSnapshot(); + }); + }); + describe('when rendering two forks, and one fork has an extra long tine', () => { + beforeEach(() => { const payload = { data: { result: { @@ -150,36 +227,8 @@ describe('resolver graph layout', () => { processH, ]); }); - it('the width of process subtress is calculated correctly', () => { - const expected = new Map([ - [processA, 3 * distanceBetweenNodes], - [processB, 1 * distanceBetweenNodes], - [processC, 1 * distanceBetweenNodes], - [processD, 0 * distanceBetweenNodes], - [processE, 0 * distanceBetweenNodes], - [processF, 0 * distanceBetweenNodes], - [processG, 0 * distanceBetweenNodes], - [processH, 0 * distanceBetweenNodes], - ]); - const actual = widthOfProcessSubtrees(store.getState()); - expect(actual).toEqual(expected); - }); - it('it renders the nodes at the right positions', () => { - const expected = new Map([ - [processA, [0, 100]], - [processB, [-100, 0]], - [processC, [100, 0]], - [processD, [-150, -100]], - [processE, [-50, -100]], - [processF, [50, -100]], - [processG, [150, -100]], - [processH, [150, -200]], - ]); - const actual = processNodePositionsAndEdgeLineSegments(store.getState()).processNodePositions; - expect(actual).toEqual(expected); - }); - it('it renders edges at the right positions', () => { - expect(false).toEqual(true); + it('renders right', () => { + expect(processNodePositionsAndEdgeLineSegments(store.getState())).toMatchSnapshot(); }); }); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 6cee7d640bb74c..976026e80655e9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -17,6 +17,10 @@ import { const unit = 100; const distanceBetweenNodesInUnits = 1; + +/** + * The distance in pixels (at scale 1) between nodes. Change this to space out nodes more + */ export const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; function yHalfWayBetweenSourceAndTarget(sourcePosition: Vector2, targetPosition: Vector2) { @@ -94,7 +98,8 @@ const graphableProcessesPidMaps = createSelector( }; } ); -export const widthOfProcessSubtrees = createSelector( + +const widthOfProcessSubtrees = createSelector( graphableProcesses, graphableProcessesPidMaps, function widthOfProcessSubtrees( @@ -103,18 +108,25 @@ export const widthOfProcessSubtrees = createSelector( graphableProcessesPidMaps /* eslint-enable no-shadow */ ) { + const widths = new Map(); + + if (graphableProcesses.length === 0) { + return widths; + } + const processesInReverseLevelOrder = [ ...levelOrder(graphableProcesses[0], childrenOfProcess), ].reverse(); - const widths = new Map(); - for (const process of processesInReverseLevelOrder) { const children = childrenOfProcess(process); const sumOfWidthOfChildren = function sumOfWidthOfChildren() { return children.reduce(function sum(currentValue, child) { - return currentValue + widths.get(child); + /** + * widths.get will always be defined because we are populating it in reverse level order. + */ + return currentValue + widths.get(child)!; }, 0); }; @@ -145,113 +157,118 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( widthOfProcessSubtrees /* eslint-enable no-shadow */ ) { - const positions = new Map(); + const positions = new Map(); const edgeLineSegments = []; let parentProcess: ProcessEvent | undefined; let numberOfPrecedingSiblings = 0; let runningWidthOfPrecedingSiblings = 0; - for (const process of levelOrder(graphableProcesses[0], childrenOfProcess)) { - if (parentProcess === undefined) { - parentProcess = process; - numberOfPrecedingSiblings = 0; - runningWidthOfPrecedingSiblings = 0; - const yOffset = distanceBetweenNodes * 1; - positions.set(process, [0, yOffset]); - } else { - if (parentProcess !== parentOfProcess(process)) { - parentProcess = parentOfProcess(process); + + if (graphableProcesses.length !== 0) { + for (const process of levelOrder(graphableProcesses[0], childrenOfProcess)) { + if (parentProcess === undefined) { + parentProcess = process; numberOfPrecedingSiblings = 0; runningWidthOfPrecedingSiblings = 0; - } - - const xOffset = - widthOfProcessSubtrees.get(parentProcess) / -2 + - numberOfPrecedingSiblings * distanceBetweenNodes + - runningWidthOfPrecedingSiblings + - widthOfProcessSubtrees.get(process) / 2; - - const position = vector2Add([xOffset, -distanceBetweenNodes], positions.get(parentProcess)); + positions.set(process, [0, 0]); + } else { + if (parentProcess !== parentOfProcess(process)) { + parentProcess = parentOfProcess(process); + numberOfPrecedingSiblings = 0; + runningWidthOfPrecedingSiblings = 0; + } - positions.set(process, position); + const xOffset = + widthOfProcessSubtrees.get(parentProcess) / -2 + + numberOfPrecedingSiblings * distanceBetweenNodes + + runningWidthOfPrecedingSiblings + + widthOfProcessSubtrees.get(process) / 2; + + const position = vector2Add( + [xOffset, -distanceBetweenNodes], + positions.get(parentProcess) + ); + + positions.set(process, position); + + const edgeLineSegmentsForProcess = function edgeLineSegmentsForProcess() { + const parentProcessPosition = positions.get(parentProcess); + const midwayY = yHalfWayBetweenSourceAndTarget(parentProcessPosition, position); + + // If this is the first child + if (numberOfPrecedingSiblings === 0) { + if (isProcessOnlyChild(process)) { + // add a single line segment directly from parent to child + return [[parentProcessPosition, position]]; + } else { + // Draw 3 line segments + // One from the parent to the midway line, + // The midway line (a horizontal line the width of the parent, halfway between the parent and child) + // A line from the child to the midway line + return [ + lineFromParentToMidwayLine(), + midwayLine(parentProcess), + lineFromProcessToMidwayLine(), + ]; + } + } else { + // If this isn't the first child, it must have siblings (the first of which drew the midway line and line + // from the parent to the midway line + return [lineFromProcessToMidwayLine()]; + } - const edgeLineSegmentsForProcess = function edgeLineSegmentsForProcess() { - const parentProcessPosition = positions.get(parentProcess); - const midwayY = yHalfWayBetweenSourceAndTarget(parentProcessPosition, position); + function lineFromParentToMidwayLine() { + return [ + // Add a line from parent to midway point + parentProcessPosition, + [parentProcessPosition[0], midwayY], + ]; + } - // If this is the first child - if (numberOfPrecedingSiblings === 0) { - if (isProcessOnlyChild(process)) { - // add a single line segment directly from parent to child - return [[parentProcessPosition, position]]; - } else { - // Draw 3 line segments - // One from the parent to the midway line, - // The midway line (a horizontal line the width of the parent, halfway between the parent and child) - // A line from the child to the midway line + function lineFromProcessToMidwayLine() { return [ - lineFromParentToMidwayLine(), - midwayLine(parentProcess), - lineFromProcessToMidwayLine(), + [ + position[0], + // Simulate a capped line by moving this up a bit so it overlaps with the midline segment + midwayY, + ], + position, ]; } - } else { - // If this isn't the first child, it must have siblings (the first of which drew the midway line and line - // from the parent to the midway line - return [lineFromProcessToMidwayLine()]; - } - function lineFromParentToMidwayLine() { - return [ - // Add a line from parent to midway point - parentProcessPosition, - [parentProcessPosition[0], midwayY], - ]; - } + function midwayLine(parentProcessNode: ProcessEvent) { + /* eslint-disable no-shadow */ + const parentProcessPosition = positions.get(parentProcessNode); + /* eslint-enable no-shadow */ + const childrenOfParent = childrenOfProcess(parentProcessNode); + const lastChild = childrenOfParent[childrenOfParent.length - 1]; - function lineFromProcessToMidwayLine() { - return [ - [ - position[0], - // Simulate a capped line by moving this up a bit so it overlaps with the midline segment - midwayY, - ], - position, - ]; - } + const widthOfParent = widthOfProcessSubtrees.get(parentProcessNode); + const widthOfFirstChild = widthOfProcessSubtrees.get(process); + const widthOfLastChild = widthOfProcessSubtrees.get(lastChild); + const widthOfMidline = widthOfParent - widthOfFirstChild / 2 - widthOfLastChild / 2; - function midwayLine(parentProcessNode: ProcessEvent) { - /* eslint-disable no-shadow */ - const parentProcessPosition = positions.get(parentProcessNode); - /* eslint-enable no-shadow */ - const childrenOfParent = childrenOfProcess(parentProcessNode); - const lastChild = childrenOfParent[childrenOfParent.length - 1]; - - const widthOfParent = widthOfProcessSubtrees.get(parentProcessNode); - const widthOfFirstChild = widthOfProcessSubtrees.get(process); - const widthOfLastChild = widthOfProcessSubtrees.get(lastChild); - const widthOfMidline = widthOfParent - widthOfFirstChild / 2 - widthOfLastChild / 2; - - const minX = widthOfParent / -2 + widthOfFirstChild / 2; - const maxX = minX + widthOfMidline; - - return [ - [ - // Position line relative to the parent's x component - parentProcessPosition[0] + minX, - midwayY, - ], - [ - // Position line relative to the parent's x component - parentProcessPosition[0] + maxX, - midwayY, - ], - ]; - } - }; + const minX = widthOfParent / -2 + widthOfFirstChild / 2; + const maxX = minX + widthOfMidline; - edgeLineSegments.push(...edgeLineSegmentsForProcess()); - numberOfPrecedingSiblings += 1; - runningWidthOfPrecedingSiblings += widthOfProcessSubtrees.get(process); + return [ + [ + // Position line relative to the parent's x component + parentProcessPosition[0] + minX, + midwayY, + ], + [ + // Position line relative to the parent's x component + parentProcessPosition[0] + maxX, + midwayY, + ], + ]; + } + }; + + edgeLineSegments.push(...edgeLineSegmentsForProcess()); + numberOfPrecedingSiblings += 1; + runningWidthOfPrecedingSiblings += widthOfProcessSubtrees.get(process); + } } } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx deleted file mode 100644 index b6e70c18a46285..00000000000000 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/diagnostic_dot.tsx +++ /dev/null @@ -1,42 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { useSelector } from 'react-redux'; -import { Vector2 } from '../types'; -import { applyMatrix3 } from '../lib/vector2'; -import * as selectors from '../store/selectors'; - -export 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; -`; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx index 8b50955c1b55f9..48138e1ef69496 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx @@ -7,7 +7,7 @@ import React from 'react'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; -import { applyMatrix3 } from '../lib/vector2'; +import { applyMatrix3, distance, angle } from '../lib/vector2'; import { Vector2 } from '../types'; import * as selectors from '../store/selectors'; @@ -23,24 +23,16 @@ export const EdgeLine = styled( endPosition: Vector2; }) => { const projectionMatrix = useSelector(selectors.projectionMatrix); - const [left, top] = applyMatrix3(startPosition, projectionMatrix); - const length = distance(startPosition[0], startPosition[1], endPosition[0], endPosition[1]); - const deltaX = endPosition[0] - startPosition[0]; - const deltaY = endPosition[1] - startPosition[1]; - const angle = -Math.atan2(deltaY, deltaX); - /** - * https://www.mathsisfun.com/algebra/distance-2-points.html - */ - function distance(x1, y1, x2, y2) { - return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); - } + const screenStart = applyMatrix3(startPosition, projectionMatrix); + const screenEnd = applyMatrix3(endPosition, projectionMatrix); + const length = distance(screenStart, screenEnd); const style = { - left: left + 'px', - top: top + 'px', + left: screenStart[0] + 'px', + top: screenStart[1] + 'px', width: length + 'px', transformOrigin: 'top left', - transform: `translateY(-50%) rotateZ(${angle}rad)`, + transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`, }; 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 f0a02459450ff8..1c8efc3aeeb3c7 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState, useEffect, useMemo } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { Store } from 'redux'; import { Provider, useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; @@ -12,7 +12,6 @@ import { ResolverState, ResolverAction } from '../types'; import * as selectors from '../store/selectors'; import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect'; import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler'; -import { DiagnosticDot } from './diagnostic_dot'; import { ProcessEventDot } from './process_event_dot'; import { EdgeLine } from './edge_line'; @@ -127,21 +126,6 @@ const Resolver = styled( }; }, [handleMouseMove]); - const dotPositions = useMemo( - (): ReadonlyArray => [ - [0, 0], - [0, 100], - [100, 100], - [100, 0], - [100, -100], - [0, -100], - [-100, -100], - [-100, 0], - [-100, 100], - ], - [] - ); - const refCallback = useCallback( (node: null | HTMLDivElement) => { setRef(node); From b6da356ccee72522930e10013dcc5ce975c6cb8d Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 8 Jan 2020 15:50:05 -0500 Subject: [PATCH 63/86] about to replace graphableProcessPidMaps --- .../resolver/lib/indexed_process_tree.ts | 70 +++++++++++++++++++ .../public/embeddables/resolver/types.ts | 8 +++ 2 files changed, 78 insertions(+) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/lib/indexed_process_tree.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/indexed_process_tree.ts new file mode 100644 index 00000000000000..03c8e68c65adf5 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/indexed_process_tree.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { uniquePidForProcess, uniqueParentPidForProcess } from '../models/process_event'; +import { IndexedProcessTree, ProcessEvent } from '../types'; + +/** + * Create a new `IndexedProcessTree` + * TODO, what is this? + */ +export function factory(processes: ProcessEvent[]): IndexedProcessTree { + const idToChildren = new Map(); + const idToValue = new Map(); + + for (const process of processes) { + idToValue.set(uniquePidForProcess(process), process); + const uniqueParentPid = uniqueParentPidForProcess(process); + const processChildren = idToChildren.get(uniqueParentPid); + if (processChildren) { + processChildren.push(process); + } else { + idToChildren.set(uniqueParentPid, [process]); + } + } + + return { + idToChildren, + idToProcess: idToValue, + }; +} + +/** + * Returns an array with any children `ProcessEvent`s of the passed in `process` + */ +export function children(tree: IndexedProcessTree, process: ProcessEvent): ProcessEvent[] { + const id = uniquePidForProcess(process); + const processChildren = tree.idToChildren.get(id); + return processChildren === undefined ? [] : processChildren; +} + +/** + * Returns the parent ProcessEvent, if any, for the passed in `childProcess` + */ +export function parent( + tree: IndexedProcessTree, + childProcess: ProcessEvent +): ProcessEvent | undefined { + const uniqueParentPid = uniqueParentPidForProcess(childProcess); + if (uniqueParentPid === undefined) { + return undefined; + } else { + return tree.idToProcess.get(uniqueParentPid); + } +} + +/** + * Returns true if the `childProcess` has no siblings + */ +export function isOnlyChild(tree: IndexedProcessTree, childProcess: ProcessEvent) { + const parentProcess = parent(tree, childProcess); + if (parentProcess === undefined) { + // if parent process is undefined, then the child is the root. We choose not to support multiple roots + return true; + } else { + return children(tree, parentProcess).length === 1; + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index b03dc91a0bcf38..19107d9a5e2162 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -124,6 +124,14 @@ export interface ProcessEvent { }; } +export interface IndexedProcessTree { + /** + * `id` can be undefined because `source_id` can be undefined. Root nodes have no known parent + */ + idToChildren: Map; + idToProcess: Map; +} + export interface GraphableProcessesPidMaps { processesByUniqueParentPid: Map; processesByUniquePid: Map; From 83477492a3fcbe280c7353b58b62b0f4e298e333 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 8 Jan 2020 16:04:09 -0500 Subject: [PATCH 64/86] tiny refactor --- .../{lib => models}/indexed_process_tree.ts | 2 +- .../resolver/store/data/selectors.ts | 136 ++++-------------- 2 files changed, 28 insertions(+), 110 deletions(-) rename x-pack/plugins/endpoint/public/embeddables/resolver/{lib => models}/indexed_process_tree.ts (98%) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts similarity index 98% rename from x-pack/plugins/endpoint/public/embeddables/resolver/lib/indexed_process_tree.ts rename to x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index 03c8e68c65adf5..832b84ea0d4257 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uniquePidForProcess, uniqueParentPidForProcess } from '../models/process_event'; +import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; import { IndexedProcessTree, ProcessEvent } from '../types'; /** diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 976026e80655e9..16d17e23e94193 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -5,15 +5,17 @@ */ import { createSelector } from 'reselect'; -import { DataState, ProcessEvent, GraphableProcessesPidMaps } from '../../types'; +import { DataState, ProcessEvent } from '../../types'; import { levelOrder } from '../../lib/tree_sequencers'; import { Vector2 } from '../../types'; import { add as vector2Add } from '../../lib/vector2'; +import { isGraphableProcess } from '../../models/process_event'; import { - isGraphableProcess, - uniquePidForProcess, - uniqueParentPidForProcess, -} from '../../models/process_event'; + factory as indexedProcessTreeFactory, + children as indexedProcessTreeChildren, + parent as indexedProcessTreeParent, + isOnlyChild as indexedProcessTreeIsOnlyChild, +} from '../../models/indexed_process_tree'; const unit = 100; const distanceBetweenNodesInUnits = 1; @@ -27,85 +29,19 @@ function yHalfWayBetweenSourceAndTarget(sourcePosition: Vector2, targetPosition: return sourcePosition[1] + (targetPosition[1] - sourcePosition[1]) / 2; } -function childrenOfProcessFromGraphableProcessesPidMaps( - graphableProcessesPidMaps: GraphableProcessesPidMaps, - parentProcess: ProcessEvent -) { - const uniqueParentPid = uniquePidForProcess(parentProcess); - const children = graphableProcessesPidMaps.processesByUniqueParentPid.get(uniqueParentPid); - return children === undefined ? [] : children; -} - -function parentOfProcessFromGraphableProcessesPidMaps( - graphableProcessesPidMaps: GraphableProcessesPidMaps, - childProcess: ProcessEvent -): ProcessEvent | undefined { - const uniqueParentPid = uniqueParentPidForProcess(childProcess); - if (uniqueParentPid === undefined) { - return undefined; - } else { - return graphableProcessesPidMaps.processesByUniquePid.get(uniqueParentPid); - } -} - -function isProcessOnlyChildFromGraphableProcessPidMaps( - graphableProcessesPidMaps: GraphableProcessesPidMaps, - childProcess: ProcessEvent -) { - const parentProcess = parentOfProcessFromGraphableProcessesPidMaps( - graphableProcessesPidMaps, - childProcess - ); - if (parentProcess === undefined) { - // if parent process is undefined, then the child is the root. We choose not to support multiple roots - return true; - } else { - return ( - childrenOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, parentProcess) - .length === 1 - ); - } -} - export function graphableProcesses(state: DataState) { return state.results.filter(isGraphableProcess); } -const graphableProcessesPidMaps = createSelector( - graphableProcesses, - function graphableProcessesPidMaps( - /* eslint-disable no-shadow */ - graphableProcesses - /* eslint-disable no-shadow */ - ): GraphableProcessesPidMaps { - const processesByUniqueParentPid = new Map(); - const processesByUniquePid = new Map(); - - for (const process of graphableProcesses) { - processesByUniquePid.set(uniquePidForProcess(process), process); - const uniqueParentPid = uniqueParentPidForProcess(process); - const processes = processesByUniqueParentPid.get(uniqueParentPid); - if (processes) { - processes.push(process); - } else { - processesByUniqueParentPid.set(uniqueParentPid, [process]); - } - } - - return { - processesByUniqueParentPid, - processesByUniquePid, - }; - } -); +const indexedProcessTree = createSelector(graphableProcesses, indexedProcessTreeFactory); const widthOfProcessSubtrees = createSelector( graphableProcesses, - graphableProcessesPidMaps, + indexedProcessTree, function widthOfProcessSubtrees( /* eslint-disable no-shadow */ graphableProcesses, - graphableProcessesPidMaps + indexedProcessTree /* eslint-enable no-shadow */ ) { const widths = new Map(); @@ -115,11 +51,13 @@ const widthOfProcessSubtrees = createSelector( } const processesInReverseLevelOrder = [ - ...levelOrder(graphableProcesses[0], childrenOfProcess), + ...levelOrder(graphableProcesses[0], (child: ProcessEvent) => + indexedProcessTreeChildren(indexedProcessTree, child) + ), ].reverse(); for (const process of processesInReverseLevelOrder) { - const children = childrenOfProcess(process); + const children = indexedProcessTreeChildren(indexedProcessTree, process); const sumOfWidthOfChildren = function sumOfWidthOfChildren() { return children.reduce(function sum(currentValue, child) { @@ -136,24 +74,17 @@ const widthOfProcessSubtrees = createSelector( } return widths; - - function childrenOfProcess(parentProcess: ProcessEvent) { - return childrenOfProcessFromGraphableProcessesPidMaps( - graphableProcessesPidMaps, - parentProcess - ); - } } ); export const processNodePositionsAndEdgeLineSegments = createSelector( graphableProcesses, - graphableProcessesPidMaps, + indexedProcessTree, widthOfProcessSubtrees, function processNodePositionsAndEdgeLineSegments( /* eslint-disable no-shadow */ graphableProcesses, - graphableProcessesPidMaps, + indexedProcessTree, widthOfProcessSubtrees /* eslint-enable no-shadow */ ) { @@ -164,15 +95,18 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( let runningWidthOfPrecedingSiblings = 0; if (graphableProcesses.length !== 0) { - for (const process of levelOrder(graphableProcesses[0], childrenOfProcess)) { + for (const process of levelOrder(graphableProcesses[0], (child: ProcessEvent) => + indexedProcessTreeChildren(indexedProcessTree, child) + )) { if (parentProcess === undefined) { parentProcess = process; numberOfPrecedingSiblings = 0; runningWidthOfPrecedingSiblings = 0; positions.set(process, [0, 0]); } else { - if (parentProcess !== parentOfProcess(process)) { - parentProcess = parentOfProcess(process); + const currentParent = indexedProcessTreeParent(indexedProcessTree, process); + if (parentProcess !== currentParent) { + parentProcess = currentParent; numberOfPrecedingSiblings = 0; runningWidthOfPrecedingSiblings = 0; } @@ -196,7 +130,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( // If this is the first child if (numberOfPrecedingSiblings === 0) { - if (isProcessOnlyChild(process)) { + if (indexedProcessTreeIsOnlyChild(indexedProcessTree, process)) { // add a single line segment directly from parent to child return [[parentProcessPosition, position]]; } else { @@ -239,7 +173,10 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( /* eslint-disable no-shadow */ const parentProcessPosition = positions.get(parentProcessNode); /* eslint-enable no-shadow */ - const childrenOfParent = childrenOfProcess(parentProcessNode); + const childrenOfParent = indexedProcessTreeChildren( + indexedProcessTree, + parentProcessNode + ); const lastChild = childrenOfParent[childrenOfParent.length - 1]; const widthOfParent = widthOfProcessSubtrees.get(parentProcessNode); @@ -272,25 +209,6 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( } } - function childrenOfProcess( - /* eslint-disable no-shadow */ - parentProcess: ProcessEvent - /* eslint-enable no-shadow */ - ) { - return childrenOfProcessFromGraphableProcessesPidMaps( - graphableProcessesPidMaps, - parentProcess - ); - } - - function parentOfProcess(childProcess: ProcessEvent) { - return parentOfProcessFromGraphableProcessesPidMaps(graphableProcessesPidMaps, childProcess); - } - - function isProcessOnlyChild(childProcess: ProcessEvent) { - return isProcessOnlyChildFromGraphableProcessPidMaps(graphableProcessesPidMaps, childProcess); - } - return { processNodePositions: positions, edgeLineSegments, From f9ac3485567ae0da4419de6cf919aa14202d13f0 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Wed, 8 Jan 2020 16:42:16 -0500 Subject: [PATCH 65/86] working on refactoring the layout code --- .../resolver/models/indexed_process_tree.ts | 32 ++++++ .../resolver/store/data/selectors.ts | 106 ++++++++---------- 2 files changed, 79 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index 832b84ea0d4257..e8dc731d5e8bb3 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -6,6 +6,7 @@ import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; import { IndexedProcessTree, ProcessEvent } from '../types'; +import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; /** * Create a new `IndexedProcessTree` @@ -68,3 +69,34 @@ export function isOnlyChild(tree: IndexedProcessTree, childProcess: ProcessEvent return children(tree, parentProcess).length === 1; } } + +/** + * Number of processes in the tree + */ +export function size(tree: IndexedProcessTree) { + return tree.idToProcess.size; +} + +/** + * Return the root process + */ +export function root(tree: IndexedProcessTree) { + if (size(tree) === 0) { + return null; + } + let current: ProcessEvent = tree.idToProcess.values().next().value; + while (parent(tree, current) !== undefined) { + current = parent(tree, current)!; + } + return current; +} + +/** + * Yield processes in level order + */ +export function* levelOrder(tree: IndexedProcessTree) { + const rootNode = root(tree); + if (rootNode !== null) { + yield* baseLevelOrder(rootNode, children.bind(null, tree)); + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 16d17e23e94193..b6ccfccc982398 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -5,7 +5,7 @@ */ import { createSelector } from 'reselect'; -import { DataState, ProcessEvent } from '../../types'; +import { DataState, ProcessEvent, IndexedProcessTree } from '../../types'; import { levelOrder } from '../../lib/tree_sequencers'; import { Vector2 } from '../../types'; import { add as vector2Add } from '../../lib/vector2'; @@ -15,6 +15,8 @@ import { children as indexedProcessTreeChildren, parent as indexedProcessTreeParent, isOnlyChild as indexedProcessTreeIsOnlyChild, + size, + levelOrder as indexedProcessTreeLevelOrder, } from '../../models/indexed_process_tree'; const unit = 100; @@ -25,69 +27,51 @@ const distanceBetweenNodesInUnits = 1; */ export const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; -function yHalfWayBetweenSourceAndTarget(sourcePosition: Vector2, targetPosition: Vector2) { - return sourcePosition[1] + (targetPosition[1] - sourcePosition[1]) / 2; -} - export function graphableProcesses(state: DataState) { return state.results.filter(isGraphableProcess); } -const indexedProcessTree = createSelector(graphableProcesses, indexedProcessTreeFactory); - -const widthOfProcessSubtrees = createSelector( - graphableProcesses, - indexedProcessTree, - function widthOfProcessSubtrees( - /* eslint-disable no-shadow */ - graphableProcesses, - indexedProcessTree - /* eslint-enable no-shadow */ - ) { - const widths = new Map(); +function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree) { + const widths = new Map(); - if (graphableProcesses.length === 0) { - return widths; - } + if (size(indexedProcessTree) === 0) { + return widths; + } - const processesInReverseLevelOrder = [ - ...levelOrder(graphableProcesses[0], (child: ProcessEvent) => - indexedProcessTreeChildren(indexedProcessTree, child) - ), - ].reverse(); - - for (const process of processesInReverseLevelOrder) { - const children = indexedProcessTreeChildren(indexedProcessTree, process); - - const sumOfWidthOfChildren = function sumOfWidthOfChildren() { - return children.reduce(function sum(currentValue, child) { - /** - * widths.get will always be defined because we are populating it in reverse level order. - */ - return currentValue + widths.get(child)!; - }, 0); - }; - - const width = - sumOfWidthOfChildren() + Math.max(0, children.length - 1) * distanceBetweenNodes; - widths.set(process, width); - } + const processesInReverseLevelOrder = [ + ...indexedProcessTreeLevelOrder(indexedProcessTree), + ].reverse(); + + for (const process of processesInReverseLevelOrder) { + const children = indexedProcessTreeChildren(indexedProcessTree, process); + + const sumOfWidthOfChildren = function sumOfWidthOfChildren() { + return children.reduce(function sum(currentValue, child) { + /** + * widths.get will always be defined because we are populating it in reverse level order. + * TODO + */ + return currentValue + widths.get(child)!; + }, 0); + }; - return widths; + const width = sumOfWidthOfChildren() + Math.max(0, children.length - 1) * distanceBetweenNodes; + widths.set(process, width); } -); + + return widths; +} export const processNodePositionsAndEdgeLineSegments = createSelector( graphableProcesses, - indexedProcessTree, - widthOfProcessSubtrees, function processNodePositionsAndEdgeLineSegments( /* eslint-disable no-shadow */ - graphableProcesses, - indexedProcessTree, - widthOfProcessSubtrees + graphableProcesses /* eslint-enable no-shadow */ ) { + const indexedProcessTree = indexedProcessTreeFactory(graphableProcesses); + const widths = widthsOfProcessSubtrees(indexedProcessTree); + const positions = new Map(); const edgeLineSegments = []; let parentProcess: ProcessEvent | undefined; @@ -95,9 +79,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( let runningWidthOfPrecedingSiblings = 0; if (graphableProcesses.length !== 0) { - for (const process of levelOrder(graphableProcesses[0], (child: ProcessEvent) => - indexedProcessTreeChildren(indexedProcessTree, child) - )) { + for (const process of indexedProcessTreeLevelOrder(indexedProcessTree)) { if (parentProcess === undefined) { parentProcess = process; numberOfPrecedingSiblings = 0; @@ -111,11 +93,16 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( runningWidthOfPrecedingSiblings = 0; } + if (!parentProcess) { + // since we iterate in level order, this can't happened + throw new Error(); + } + const xOffset = - widthOfProcessSubtrees.get(parentProcess) / -2 + + widths.get(parentProcess) / -2 + numberOfPrecedingSiblings * distanceBetweenNodes + runningWidthOfPrecedingSiblings + - widthOfProcessSubtrees.get(process) / 2; + widths.get(process) / 2; const position = vector2Add( [xOffset, -distanceBetweenNodes], @@ -126,7 +113,8 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( const edgeLineSegmentsForProcess = function edgeLineSegmentsForProcess() { const parentProcessPosition = positions.get(parentProcess); - const midwayY = yHalfWayBetweenSourceAndTarget(parentProcessPosition, position); + + const midwayY = parentProcessPosition[1] + (position[1] - parentProcessPosition[1]) / 2; // If this is the first child if (numberOfPrecedingSiblings === 0) { @@ -179,9 +167,9 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( ); const lastChild = childrenOfParent[childrenOfParent.length - 1]; - const widthOfParent = widthOfProcessSubtrees.get(parentProcessNode); - const widthOfFirstChild = widthOfProcessSubtrees.get(process); - const widthOfLastChild = widthOfProcessSubtrees.get(lastChild); + const widthOfParent = widths.get(parentProcessNode); + const widthOfFirstChild = widths.get(process); + const widthOfLastChild = widths.get(lastChild); const widthOfMidline = widthOfParent - widthOfFirstChild / 2 - widthOfLastChild / 2; const minX = widthOfParent / -2 + widthOfFirstChild / 2; @@ -204,7 +192,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( edgeLineSegments.push(...edgeLineSegmentsForProcess()); numberOfPrecedingSiblings += 1; - runningWidthOfPrecedingSiblings += widthOfProcessSubtrees.get(process); + runningWidthOfPrecedingSiblings += widths.get(process); } } } From feaf213613f51106bdc3f828d8f2d954d014118c Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 10:13:30 -0500 Subject: [PATCH 66/86] big ol refactor --- .../resolver/store/data/selectors.ts | 115 +++++++++++++++--- 1 file changed, 95 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index b6ccfccc982398..e1fa70c8c0fe7f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -6,7 +6,6 @@ import { createSelector } from 'reselect'; import { DataState, ProcessEvent, IndexedProcessTree } from '../../types'; -import { levelOrder } from '../../lib/tree_sequencers'; import { Vector2 } from '../../types'; import { add as vector2Add } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; @@ -16,7 +15,7 @@ import { parent as indexedProcessTreeParent, isOnlyChild as indexedProcessTreeIsOnlyChild, size, - levelOrder as indexedProcessTreeLevelOrder, + levelOrder, } from '../../models/indexed_process_tree'; const unit = 100; @@ -38,9 +37,7 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree) { return widths; } - const processesInReverseLevelOrder = [ - ...indexedProcessTreeLevelOrder(indexedProcessTree), - ].reverse(); + const processesInReverseLevelOrder = [...levelOrder(indexedProcessTree)].reverse(); for (const process of processesInReverseLevelOrder) { const children = indexedProcessTreeChildren(indexedProcessTree, process); @@ -48,8 +45,10 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree) { const sumOfWidthOfChildren = function sumOfWidthOfChildren() { return children.reduce(function sum(currentValue, child) { /** - * widths.get will always be defined because we are populating it in reverse level order. - * TODO + * `widths.get` will always return a number in this case. + * This loop sequences a tree in reverse level order. Width values are set for each node. + * Therefore a parent can always find a width for its children, since all of its children + * will have been handled already. */ return currentValue + widths.get(child)!; }, 0); @@ -62,6 +61,70 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree) { return widths; } +type ProcessWithWidthMetadata = { + process: ProcessEvent; + width: number; + parent: ProcessEvent; + parentWidth: number; +} & ( + | { isOnlyChild: true; lastChildWidth: null; firstChildWidth: null } + | { isOnlyChild: false; lastChildWidth: number; firstChildWidth: number } +); + +/** + * 1. calculate widths + * 2. calculate positions + * we store parent positions as we go + * 3. calculate edge lines + * we store edges as we go + * + * for ({ parent, process, parentWidth, lastChildWidth, firstChildWidth, + */ +function* levelOrderWithWidths(tree: IndexedProcessTree): Iterable { + // TODO, maybe take this in? + const widths = widthsOfProcessSubtrees(tree); + for (const process of levelOrder(tree)) { + const parent = indexedProcessTreeParent(tree, process); + const width = widths.get(process); + + if (parent === undefined) { + // TODO explain + throw new Error(); + } + const parentWidth = widths.get(parent); + + if (width === undefined || parentWidth === undefined) { + // TODO explain + throw new Error(); + } + + const thingy: Partial = { + process, + width, + parent, + parentWidth, + }; + + const siblings = indexedProcessTreeChildren(tree, parent); + if (siblings.length === 1) { + thingy.isOnlyChild = true; + thingy.lastChildWidth = null; + thingy.firstChildWidth = null; + } else { + const firstChildWidth = widths.get(siblings[0]); + const lastChildWidth = widths.get(siblings[0]); + if (firstChildWidth === undefined || lastChildWidth === undefined) { + throw new Error(); + } + thingy.isOnlyChild = false; + thingy.firstChildWidth = firstChildWidth; + thingy.lastChildWidth = lastChildWidth; + } + + yield thingy as ProcessWithWidthMetadata; + } +} + export const processNodePositionsAndEdgeLineSegments = createSelector( graphableProcesses, function processNodePositionsAndEdgeLineSegments( @@ -78,8 +141,9 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( let numberOfPrecedingSiblings = 0; let runningWidthOfPrecedingSiblings = 0; + // TODO remove guard if (graphableProcesses.length !== 0) { - for (const process of indexedProcessTreeLevelOrder(indexedProcessTree)) { + for (const process of levelOrder(indexedProcessTree)) { if (parentProcess === undefined) { parentProcess = process; numberOfPrecedingSiblings = 0; @@ -93,34 +157,45 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( runningWidthOfPrecedingSiblings = 0; } + const width = widths.get(process); + if (!parentProcess) { + // TODO explain yourself + throw new Error(); + } + + const parentWidth = widths.get(parentProcess); + const parentPosition = positions.get(parentProcess); + + if ( + parentProcess === undefined || + parentWidth === undefined || + width === undefined || + parentPosition === undefined + ) { + // TODO explain more // since we iterate in level order, this can't happened throw new Error(); } const xOffset = - widths.get(parentProcess) / -2 + + parentWidth / -2 + numberOfPrecedingSiblings * distanceBetweenNodes + runningWidthOfPrecedingSiblings + - widths.get(process) / 2; + width / 2; - const position = vector2Add( - [xOffset, -distanceBetweenNodes], - positions.get(parentProcess) - ); + const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition); positions.set(process, position); const edgeLineSegmentsForProcess = function edgeLineSegmentsForProcess() { - const parentProcessPosition = positions.get(parentProcess); - - const midwayY = parentProcessPosition[1] + (position[1] - parentProcessPosition[1]) / 2; + const midwayY = parentPosition[1] + (position[1] - parentPosition[1]) / 2; // If this is the first child if (numberOfPrecedingSiblings === 0) { if (indexedProcessTreeIsOnlyChild(indexedProcessTree, process)) { // add a single line segment directly from parent to child - return [[parentProcessPosition, position]]; + return [[parentPosition, position]]; } else { // Draw 3 line segments // One from the parent to the midway line, @@ -141,8 +216,8 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( function lineFromParentToMidwayLine() { return [ // Add a line from parent to midway point - parentProcessPosition, - [parentProcessPosition[0], midwayY], + parentPosition, + [parentPosition[0], midwayY], ]; } From a438da688a6fb1d00e3aeb91099050fd4831923a Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 10:24:50 -0500 Subject: [PATCH 67/86] we are cool --- .../resolver/store/data/selectors.ts | 96 ++++++++++++------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index e1fa70c8c0fe7f..9bfaa234ea0484 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -61,16 +61,26 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree) { return widths; } +// bet sean loves this type ProcessWithWidthMetadata = { process: ProcessEvent; width: number; - parent: ProcessEvent; - parentWidth: number; } & ( - | { isOnlyChild: true; lastChildWidth: null; firstChildWidth: null } - | { isOnlyChild: false; lastChildWidth: number; firstChildWidth: number } + | ({ + parent: ProcessEvent; + parentWidth: number; + } & ( + | { isOnlyChild: true; firstChildWidth: null; lastChildWidth: null } + | { isOnlyChild: false; firstChildWidth: number; lastChildWidth: number } + )) + | { + parent: null; + parentWidth: null; + isOnlyChild: null; + lastChildWidth: null; + firstChildWidth: null; + } ); - /** * 1. calculate widths * 2. calculate positions @@ -80,48 +90,62 @@ type ProcessWithWidthMetadata = { * * for ({ parent, process, parentWidth, lastChildWidth, firstChildWidth, */ -function* levelOrderWithWidths(tree: IndexedProcessTree): Iterable { - // TODO, maybe take this in? - const widths = widthsOfProcessSubtrees(tree); +function* levelOrderWithWidths( + tree: IndexedProcessTree, + widths: ReturnType +): Iterable { for (const process of levelOrder(tree)) { const parent = indexedProcessTreeParent(tree, process); const width = widths.get(process); - if (parent === undefined) { - // TODO explain - throw new Error(); - } - const parentWidth = widths.get(parent); - - if (width === undefined || parentWidth === undefined) { + if (width === undefined) { // TODO explain throw new Error(); } - const thingy: Partial = { - process, - width, - parent, - parentWidth, - }; - - const siblings = indexedProcessTreeChildren(tree, parent); - if (siblings.length === 1) { - thingy.isOnlyChild = true; - thingy.lastChildWidth = null; - thingy.firstChildWidth = null; + if (parent === undefined) { + yield { + process, + width, + parent: null, + parentWidth: null, + isOnlyChild: null, + firstChildWidth: null, + lastChildWidth: null, + }; } else { - const firstChildWidth = widths.get(siblings[0]); - const lastChildWidth = widths.get(siblings[0]); - if (firstChildWidth === undefined || lastChildWidth === undefined) { + const parentWidth = widths.get(parent); + + if (parentWidth === undefined) { + // TODO explain throw new Error(); } - thingy.isOnlyChild = false; - thingy.firstChildWidth = firstChildWidth; - thingy.lastChildWidth = lastChildWidth; - } - yield thingy as ProcessWithWidthMetadata; + const thingy: Partial = { + process, + width, + parent, + parentWidth, + }; + + const siblings = indexedProcessTreeChildren(tree, parent); + if (siblings.length === 1) { + thingy.isOnlyChild = true; + thingy.lastChildWidth = null; + thingy.firstChildWidth = null; + } else { + const firstChildWidth = widths.get(siblings[0]); + const lastChildWidth = widths.get(siblings[0]); + if (firstChildWidth === undefined || lastChildWidth === undefined) { + throw new Error(); + } + thingy.isOnlyChild = false; + thingy.firstChildWidth = firstChildWidth; + thingy.lastChildWidth = lastChildWidth; + } + + yield thingy as ProcessWithWidthMetadata; + } } } @@ -143,7 +167,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( // TODO remove guard if (graphableProcesses.length !== 0) { - for (const process of levelOrder(indexedProcessTree)) { + for (const { process } of levelOrderWithWidths(indexedProcessTree, widths)) { if (parentProcess === undefined) { parentProcess = process; numberOfPrecedingSiblings = 0; From 261e0f1d692d2987a90bc9d526f0ac8db2ff9b37 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 10:39:41 -0500 Subject: [PATCH 68/86] megafactor --- .../resolver/store/data/selectors.ts | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 9bfaa234ea0484..a873c74c90cd0c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -161,44 +161,34 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( const positions = new Map(); const edgeLineSegments = []; - let parentProcess: ProcessEvent | undefined; + // Keep track of last processed parent so we can reset parent specific counters as we iterate + let lastProcessedParentNode: ProcessEvent | undefined; let numberOfPrecedingSiblings = 0; let runningWidthOfPrecedingSiblings = 0; // TODO remove guard if (graphableProcesses.length !== 0) { - for (const { process } of levelOrderWithWidths(indexedProcessTree, widths)) { - if (parentProcess === undefined) { - parentProcess = process; - numberOfPrecedingSiblings = 0; - runningWidthOfPrecedingSiblings = 0; + for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { + // Handle root node + if (metadata.parent === null) { + const { process } = metadata; positions.set(process, [0, 0]); } else { - const currentParent = indexedProcessTreeParent(indexedProcessTree, process); - if (parentProcess !== currentParent) { - parentProcess = currentParent; + const { process, parent, width, parentWidth } = metadata; + + // Reinit counters when parent changes + if (lastProcessedParentNode !== parent) { numberOfPrecedingSiblings = 0; runningWidthOfPrecedingSiblings = 0; - } - - const width = widths.get(process); - if (!parentProcess) { - // TODO explain yourself - throw new Error(); + // keep track of this so we know when to reinitialize + lastProcessedParentNode = parent; } - const parentWidth = widths.get(parentProcess); - const parentPosition = positions.get(parentProcess); - - if ( - parentProcess === undefined || - parentWidth === undefined || - width === undefined || - parentPosition === undefined - ) { - // TODO explain more - // since we iterate in level order, this can't happened + const parentPosition = positions.get(parent); + + if (parentPosition === undefined) { + // TODO explain that this can never happen throw new Error(); } @@ -227,7 +217,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( // A line from the child to the midway line return [ lineFromParentToMidwayLine(), - midwayLine(parentProcess), + midwayLine(lastProcessedParentNode), lineFromProcessToMidwayLine(), ]; } @@ -291,7 +281,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( edgeLineSegments.push(...edgeLineSegmentsForProcess()); numberOfPrecedingSiblings += 1; - runningWidthOfPrecedingSiblings += widths.get(process); + runningWidthOfPrecedingSiblings += width; } } } From 3d89e005fe18560a4184a4970943f713cfe77d4c Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 11:09:59 -0500 Subject: [PATCH 69/86] refactor edge lines --- .../resolver/store/data/selectors.ts | 165 ++++++++++++++++-- 1 file changed, 149 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index a873c74c90cd0c..28e27ce85b30b5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -18,6 +18,10 @@ import { levelOrder, } from '../../models/indexed_process_tree'; +type ProcessWidths = Map; +type ProcessPositions = Map; +type EdgeLineSegment = Vector2[]; + const unit = 100; const distanceBetweenNodesInUnits = 1; @@ -30,7 +34,7 @@ export function graphableProcesses(state: DataState) { return state.results.filter(isGraphableProcess); } -function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree) { +function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths { const widths = new Map(); if (size(indexedProcessTree) === 0) { @@ -81,18 +85,95 @@ type ProcessWithWidthMetadata = { firstChildWidth: null; } ); -/** - * 1. calculate widths - * 2. calculate positions - * we store parent positions as we go - * 3. calculate edge lines - * we store edges as we go - * - * for ({ parent, process, parentWidth, lastChildWidth, firstChildWidth, - */ + +function processEdgeLineSegments( + indexedProcessTree: IndexedProcessTree, + widths: ProcessWidths, + positions: ProcessPositions +): EdgeLineSegment[] { + const edgeLineSegments: EdgeLineSegment[] = []; + for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { + if (metadata.parent === null) { + continue; + } + const { process, parent, parentWidth } = metadata; + const position = positions.get(process); + const parentPosition = positions.get(parent); + + if (position === undefined || parentPosition === undefined) { + throw new Error(); + } + + const midwayY = parentPosition[1] + (position[1] - parentPosition[1]) / 2; + + const lineFromProcessToMidwayLine: EdgeLineSegment = [ + [ + position[0], + // Simulate a capped line by moving this up a bit so it overlaps with the midline segment + midwayY, + ], + position, + ]; + + const siblings = indexedProcessTreeChildren(indexedProcessTree, parent); + const isFirstChild = process === siblings[0]; + + // If this is the first child + if (isFirstChild) { + // is only child? + if (metadata.isOnlyChild) { + // add a single line segment directly from parent to child + edgeLineSegments.push([parentPosition, position]); + } else { + const { firstChildWidth, lastChildWidth } = metadata; + // Draw 3 line segments + // One from the parent to the midway line, + // The midway line (a horizontal line the width of the parent, halfway between the parent and child) + // A line from the child to the midway line + // + const lineFromParentToMidwayLine = [ + // Add a line from parent to midway point + parentPosition, + [parentPosition[0], midwayY] as const, + ]; + + const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; + + const minX = parentWidth / -2 + firstChildWidth / 2; + const maxX = minX + widthOfMidline; + + const midwayLine: EdgeLineSegment = [ + [ + // Position line relative to the parent's x component + parentPosition[0] + minX, + midwayY, + ], + [ + // Position line relative to the parent's x component + parentPosition[0] + maxX, + midwayY, + ], + ]; + + edgeLineSegments.push( + /* line from parent to midway line */ + lineFromParentToMidwayLine, + midwayLine, + lineFromProcessToMidwayLine + ); + } + } else { + // If this isn't the first child, it must have siblings (the first of which drew the midway line and line + // from the parent to the midway line + edgeLineSegments.push(lineFromProcessToMidwayLine); + } + } + return edgeLineSegments; +} + function* levelOrderWithWidths( tree: IndexedProcessTree, - widths: ReturnType + widths: ProcessWidths ): Iterable { for (const process of levelOrder(tree)) { const parent = indexedProcessTreeParent(tree, process); @@ -149,6 +230,58 @@ function* levelOrderWithWidths( } } +function processPositions( + indexedProcessTree: IndexedProcessTree, + widths: ProcessWidths +): ProcessPositions { + const positions = new Map(); + // Keep track of last processed parent so we can reset parent specific counters as we iterate + let lastProcessedParentNode: ProcessEvent | undefined; + let numberOfPrecedingSiblings = 0; + let runningWidthOfPrecedingSiblings = 0; + + for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { + // Handle root node + if (metadata.parent === null) { + const { process } = metadata; + positions.set(process, [0, 0]); + } else { + const { process, parent, width, parentWidth } = metadata; + + // Reinit counters when parent changes + if (lastProcessedParentNode !== parent) { + numberOfPrecedingSiblings = 0; + runningWidthOfPrecedingSiblings = 0; + + // keep track of this so we know when to reinitialize + lastProcessedParentNode = parent; + } + + const parentPosition = positions.get(parent); + + if (parentPosition === undefined) { + // TODO explain that this can never happen + throw new Error(); + } + + const xOffset = + parentWidth / -2 + + numberOfPrecedingSiblings * distanceBetweenNodes + + runningWidthOfPrecedingSiblings + + width / 2; + + const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition); + + positions.set(process, position); + + numberOfPrecedingSiblings += 1; + runningWidthOfPrecedingSiblings += width; + } + } + + return positions; +} + export const processNodePositionsAndEdgeLineSegments = createSelector( graphableProcesses, function processNodePositionsAndEdgeLineSegments( @@ -159,8 +292,9 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( const indexedProcessTree = indexedProcessTreeFactory(graphableProcesses); const widths = widthsOfProcessSubtrees(indexedProcessTree); - const positions = new Map(); - const edgeLineSegments = []; + const positions = processPositions(indexedProcessTree, widths); + const edgeLineSegments = processEdgeLineSegments(indexedProcessTree, widths, positions); + /* // Keep track of last processed parent so we can reset parent specific counters as we iterate let lastProcessedParentNode: ProcessEvent | undefined; let numberOfPrecedingSiblings = 0; @@ -200,7 +334,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition); - positions.set(process, position); + // positions.set(process, position); const edgeLineSegmentsForProcess = function edgeLineSegmentsForProcess() { const midwayY = parentPosition[1] + (position[1] - parentPosition[1]) / 2; @@ -247,9 +381,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( } function midwayLine(parentProcessNode: ProcessEvent) { - /* eslint-disable no-shadow */ const parentProcessPosition = positions.get(parentProcessNode); - /* eslint-enable no-shadow */ const childrenOfParent = indexedProcessTreeChildren( indexedProcessTree, parentProcessNode @@ -285,6 +417,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( } } } + */ return { processNodePositions: positions, From ee2b07687c97bf5c612f0c8fc51306da5e4cf9d8 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 11:14:25 -0500 Subject: [PATCH 70/86] cleanup --- .../resolver/store/data/selectors.ts | 135 +----------------- 1 file changed, 7 insertions(+), 128 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 28e27ce85b30b5..c41fcac8a4a971 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -93,6 +93,7 @@ function processEdgeLineSegments( ): EdgeLineSegment[] { const edgeLineSegments: EdgeLineSegment[] = []; for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { + // TODO comment if (metadata.parent === null) { continue; } @@ -101,11 +102,14 @@ function processEdgeLineSegments( const parentPosition = positions.get(parent); if (position === undefined || parentPosition === undefined) { + // TODO comment throw new Error(); } + // TODO comment const midwayY = parentPosition[1] + (position[1] - parentPosition[1]) / 2; + // TODO comment const lineFromProcessToMidwayLine: EdgeLineSegment = [ [ position[0], @@ -116,11 +120,10 @@ function processEdgeLineSegments( ]; const siblings = indexedProcessTreeChildren(indexedProcessTree, parent); + // TODO, move to sequencer? const isFirstChild = process === siblings[0]; - // If this is the first child if (isFirstChild) { - // is only child? if (metadata.isOnlyChild) { // add a single line segment directly from parent to child edgeLineSegments.push([parentPosition, position]); @@ -131,10 +134,10 @@ function processEdgeLineSegments( // The midway line (a horizontal line the width of the parent, halfway between the parent and child) // A line from the child to the midway line // - const lineFromParentToMidwayLine = [ + const lineFromParentToMidwayLine: EdgeLineSegment = [ // Add a line from parent to midway point parentPosition, - [parentPosition[0], midwayY] as const, + [parentPosition[0], midwayY], ]; const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; @@ -294,130 +297,6 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( const positions = processPositions(indexedProcessTree, widths); const edgeLineSegments = processEdgeLineSegments(indexedProcessTree, widths, positions); - /* - // Keep track of last processed parent so we can reset parent specific counters as we iterate - let lastProcessedParentNode: ProcessEvent | undefined; - let numberOfPrecedingSiblings = 0; - let runningWidthOfPrecedingSiblings = 0; - - // TODO remove guard - if (graphableProcesses.length !== 0) { - for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { - // Handle root node - if (metadata.parent === null) { - const { process } = metadata; - positions.set(process, [0, 0]); - } else { - const { process, parent, width, parentWidth } = metadata; - - // Reinit counters when parent changes - if (lastProcessedParentNode !== parent) { - numberOfPrecedingSiblings = 0; - runningWidthOfPrecedingSiblings = 0; - - // keep track of this so we know when to reinitialize - lastProcessedParentNode = parent; - } - - const parentPosition = positions.get(parent); - - if (parentPosition === undefined) { - // TODO explain that this can never happen - throw new Error(); - } - - const xOffset = - parentWidth / -2 + - numberOfPrecedingSiblings * distanceBetweenNodes + - runningWidthOfPrecedingSiblings + - width / 2; - - const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition); - - // positions.set(process, position); - - const edgeLineSegmentsForProcess = function edgeLineSegmentsForProcess() { - const midwayY = parentPosition[1] + (position[1] - parentPosition[1]) / 2; - - // If this is the first child - if (numberOfPrecedingSiblings === 0) { - if (indexedProcessTreeIsOnlyChild(indexedProcessTree, process)) { - // add a single line segment directly from parent to child - return [[parentPosition, position]]; - } else { - // Draw 3 line segments - // One from the parent to the midway line, - // The midway line (a horizontal line the width of the parent, halfway between the parent and child) - // A line from the child to the midway line - return [ - lineFromParentToMidwayLine(), - midwayLine(lastProcessedParentNode), - lineFromProcessToMidwayLine(), - ]; - } - } else { - // If this isn't the first child, it must have siblings (the first of which drew the midway line and line - // from the parent to the midway line - return [lineFromProcessToMidwayLine()]; - } - - function lineFromParentToMidwayLine() { - return [ - // Add a line from parent to midway point - parentPosition, - [parentPosition[0], midwayY], - ]; - } - - function lineFromProcessToMidwayLine() { - return [ - [ - position[0], - // Simulate a capped line by moving this up a bit so it overlaps with the midline segment - midwayY, - ], - position, - ]; - } - - function midwayLine(parentProcessNode: ProcessEvent) { - const parentProcessPosition = positions.get(parentProcessNode); - const childrenOfParent = indexedProcessTreeChildren( - indexedProcessTree, - parentProcessNode - ); - const lastChild = childrenOfParent[childrenOfParent.length - 1]; - - const widthOfParent = widths.get(parentProcessNode); - const widthOfFirstChild = widths.get(process); - const widthOfLastChild = widths.get(lastChild); - const widthOfMidline = widthOfParent - widthOfFirstChild / 2 - widthOfLastChild / 2; - - const minX = widthOfParent / -2 + widthOfFirstChild / 2; - const maxX = minX + widthOfMidline; - - return [ - [ - // Position line relative to the parent's x component - parentProcessPosition[0] + minX, - midwayY, - ], - [ - // Position line relative to the parent's x component - parentProcessPosition[0] + maxX, - midwayY, - ], - ]; - } - }; - - edgeLineSegments.push(...edgeLineSegmentsForProcess()); - numberOfPrecedingSiblings += 1; - runningWidthOfPrecedingSiblings += width; - } - } - } - */ return { processNodePositions: positions, From 7b241ae082f91c1e4e3c27465f3777278bee2f4b Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 11:18:13 -0500 Subject: [PATCH 71/86] loop unravel --- .../resolver/store/data/selectors.ts | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index c41fcac8a4a971..e602c972b8108e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -123,48 +123,46 @@ function processEdgeLineSegments( // TODO, move to sequencer? const isFirstChild = process === siblings[0]; - if (isFirstChild) { - if (metadata.isOnlyChild) { - // add a single line segment directly from parent to child - edgeLineSegments.push([parentPosition, position]); - } else { - const { firstChildWidth, lastChildWidth } = metadata; - // Draw 3 line segments - // One from the parent to the midway line, - // The midway line (a horizontal line the width of the parent, halfway between the parent and child) - // A line from the child to the midway line - // - const lineFromParentToMidwayLine: EdgeLineSegment = [ - // Add a line from parent to midway point - parentPosition, - [parentPosition[0], midwayY], - ]; - - const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; - - const minX = parentWidth / -2 + firstChildWidth / 2; - const maxX = minX + widthOfMidline; - - const midwayLine: EdgeLineSegment = [ - [ - // Position line relative to the parent's x component - parentPosition[0] + minX, - midwayY, - ], - [ - // Position line relative to the parent's x component - parentPosition[0] + maxX, - midwayY, - ], - ]; - - edgeLineSegments.push( - /* line from parent to midway line */ - lineFromParentToMidwayLine, - midwayLine, - lineFromProcessToMidwayLine - ); - } + if (metadata.isOnlyChild) { + // add a single line segment directly from parent to child + edgeLineSegments.push([parentPosition, position]); + } else if (isFirstChild) { + const { firstChildWidth, lastChildWidth } = metadata; + // Draw 3 line segments + // One from the parent to the midway line, + // The midway line (a horizontal line the width of the parent, halfway between the parent and child) + // A line from the child to the midway line + // + const lineFromParentToMidwayLine: EdgeLineSegment = [ + // Add a line from parent to midway point + parentPosition, + [parentPosition[0], midwayY], + ]; + + const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; + + const minX = parentWidth / -2 + firstChildWidth / 2; + const maxX = minX + widthOfMidline; + + const midwayLine: EdgeLineSegment = [ + [ + // Position line relative to the parent's x component + parentPosition[0] + minX, + midwayY, + ], + [ + // Position line relative to the parent's x component + parentPosition[0] + maxX, + midwayY, + ], + ]; + + edgeLineSegments.push( + /* line from parent to midway line */ + lineFromParentToMidwayLine, + midwayLine, + lineFromProcessToMidwayLine + ); } else { // If this isn't the first child, it must have siblings (the first of which drew the midway line and line // from the parent to the midway line From bc26420bf8ec0524807001d24d2a311b363f2315 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 11:21:54 -0500 Subject: [PATCH 72/86] cleanup --- .../resolver/models/indexed_process_tree.ts | 13 ------------- .../embeddables/resolver/store/data/selectors.ts | 1 - 2 files changed, 14 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index e8dc731d5e8bb3..175aea72161582 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -57,19 +57,6 @@ export function parent( } } -/** - * Returns true if the `childProcess` has no siblings - */ -export function isOnlyChild(tree: IndexedProcessTree, childProcess: ProcessEvent) { - const parentProcess = parent(tree, childProcess); - if (parentProcess === undefined) { - // if parent process is undefined, then the child is the root. We choose not to support multiple roots - return true; - } else { - return children(tree, parentProcess).length === 1; - } -} - /** * Number of processes in the tree */ diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index e602c972b8108e..d636f8fcd87d95 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -13,7 +13,6 @@ import { factory as indexedProcessTreeFactory, children as indexedProcessTreeChildren, parent as indexedProcessTreeParent, - isOnlyChild as indexedProcessTreeIsOnlyChild, size, levelOrder, } from '../../models/indexed_process_tree'; From b2d8d5bf74b27af8b58d0a2e4ab24d713d309670 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 12:08:30 -0500 Subject: [PATCH 73/86] cleanup --- .../resolver/store/data/selectors.ts | 176 ++++++++++++++---- 1 file changed, 143 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index d636f8fcd87d95..0d499e1112a19b 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -33,6 +33,44 @@ export function graphableProcesses(state: DataState) { return state.results.filter(isGraphableProcess); } +/** + * In laying out the graph, we precalculate the 'width' of each subtree. The 'width' of the subtree is determined by its + * descedants and the rule that each process node must be at least 1 unit apart. Enforcing that all nodes are at least + * 1 unit apart on the x axis makes it easy to prevent the UI components from overlapping. There will always be space. + * + * Example widths: + * + * A and B each have a width of 0 + * + * A + * | + * B + * + * A has a width of 1. B and C have a width of 0. + * B and C must be 1 unit apart, so the A subtree has a width of 1. + * + * A + * ____|____ + * | | + * B C + * + * + * D, E, F, G, H all have a width of 0. + * B has a width of 1 since D->E must be 1 unit apart. + * Similarly, C has a width of 1 since F->G must be 1 unit apart. + * A has width of 3, since B has a width of 1, and C has a width of 1, and E->F must be at least + * 1 unit apart. + * A + * ____|____ + * | | + * B C + * ___|___ ___|___ + * | | | | + * D E F G + * | + * H + * + */ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths { const widths = new Map(); @@ -72,14 +110,17 @@ type ProcessWithWidthMetadata = { | ({ parent: ProcessEvent; parentWidth: number; - } & ( + } & ( // TODO bolete this | { isOnlyChild: true; firstChildWidth: null; lastChildWidth: null } | { isOnlyChild: false; firstChildWidth: number; lastChildWidth: number } )) | { parent: null; + /* Without a parent, there is no parent width */ parentWidth: null; + /* Without a parent, we can't be an only child */ isOnlyChild: null; + /** If there is no parent, there are no siblings */ lastChildWidth: null; firstChildWidth: null; } @@ -92,7 +133,9 @@ function processEdgeLineSegments( ): EdgeLineSegment[] { const edgeLineSegments: EdgeLineSegment[] = []; for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { - // TODO comment + /** + * We only handle children, drawing lines back to their parents. The root has no parent, so we skip it + */ if (metadata.parent === null) { continue; } @@ -101,39 +144,51 @@ function processEdgeLineSegments( const parentPosition = positions.get(parent); if (position === undefined || parentPosition === undefined) { - // TODO comment + /** + * All positions have been precalculated, so if any are missing, it's an error. This will never happen. + */ throw new Error(); } - // TODO comment + /** + * The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line + */ const midwayY = parentPosition[1] + (position[1] - parentPosition[1]) / 2; - // TODO comment - const lineFromProcessToMidwayLine: EdgeLineSegment = [ - [ - position[0], - // Simulate a capped line by moving this up a bit so it overlaps with the midline segment - midwayY, - ], - position, - ]; + /** + * When drawing edge lines between a parent and children (when there are multiple children) we draw a pitchfork type + * design. The 'midway' line, runs along the x axis and joins all the children with a single descendant line from the parent. + * See the ascii diagram below. The underscore characters would be the midway line. + * + * A + * ____|____ + * | | + * B C + */ + const lineFromProcessToMidwayLine: EdgeLineSegment = [[position[0], midwayY], position]; const siblings = indexedProcessTreeChildren(indexedProcessTree, parent); - // TODO, move to sequencer? const isFirstChild = process === siblings[0]; if (metadata.isOnlyChild) { - // add a single line segment directly from parent to child + // add a single line segment directly from parent to child. We don't do the 'pitchfork' in this case. edgeLineSegments.push([parentPosition, position]); } else if (isFirstChild) { + /** + * If the parent has multiple children, we draw the 'midway' line, and the line from the + * parent to the midway line, while handling the first child. + * + * Consider A the parent, and B the first child. We would draw somemthing like what's in the below diagram. The line from the + * midway line to C would be drawn when we handle C. + * + * A + * ____|____ + * | + * B C + */ const { firstChildWidth, lastChildWidth } = metadata; - // Draw 3 line segments - // One from the parent to the midway line, - // The midway line (a horizontal line the width of the parent, halfway between the parent and child) - // A line from the child to the midway line - // + const lineFromParentToMidwayLine: EdgeLineSegment = [ - // Add a line from parent to midway point parentPosition, [parentPosition[0], midwayY], ]; @@ -180,10 +235,13 @@ function* levelOrderWithWidths( const width = widths.get(process); if (width === undefined) { - // TODO explain + /** + * All widths have been precalcluated, so this will not happen. + */ throw new Error(); } + /** If the parent is undefined, we are processing the root. */ if (parent === undefined) { yield { process, @@ -198,11 +256,13 @@ function* levelOrderWithWidths( const parentWidth = widths.get(parent); if (parentWidth === undefined) { - // TODO explain + /** + * All widths have been precalcluated, so this will not happen. + */ throw new Error(); } - const thingy: Partial = { + const metadata: Partial = { process, width, parent, @@ -211,21 +271,25 @@ function* levelOrderWithWidths( const siblings = indexedProcessTreeChildren(tree, parent); if (siblings.length === 1) { - thingy.isOnlyChild = true; - thingy.lastChildWidth = null; - thingy.firstChildWidth = null; + metadata.isOnlyChild = true; + // TODO, just make these === width + metadata.lastChildWidth = null; + metadata.firstChildWidth = null; } else { const firstChildWidth = widths.get(siblings[0]); const lastChildWidth = widths.get(siblings[0]); if (firstChildWidth === undefined || lastChildWidth === undefined) { + /** + * All widths have been precalcluated, so this will not happen. + */ throw new Error(); } - thingy.isOnlyChild = false; - thingy.firstChildWidth = firstChildWidth; - thingy.lastChildWidth = lastChildWidth; + metadata.isOnlyChild = false; + metadata.firstChildWidth = firstChildWidth; + metadata.lastChildWidth = lastChildWidth; } - yield thingy as ProcessWithWidthMetadata; + yield metadata as ProcessWithWidthMetadata; } } } @@ -235,8 +299,20 @@ function processPositions( widths: ProcessWidths ): ProcessPositions { const positions = new Map(); - // Keep track of last processed parent so we can reset parent specific counters as we iterate + /** + * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent. + * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and + * reset the counters. + */ let lastProcessedParentNode: ProcessEvent | undefined; + /** + * Nodes are positioned relative to their siblings. We walk this in level order, so we handle + * children left -> right. + * + * The width of preceding siblings is used to left align the node. + * The number of preceding siblings is important because each sibling must be 1 unit apart + * on the x axis. + */ let numberOfPrecedingSiblings = 0; let runningWidthOfPrecedingSiblings = 0; @@ -244,6 +320,9 @@ function processPositions( // Handle root node if (metadata.parent === null) { const { process } = metadata; + /** + * Place the root node at (0, 0) for now. + */ positions.set(process, [0, 0]); } else { const { process, parent, width, parentWidth } = metadata; @@ -260,16 +339,32 @@ function processPositions( const parentPosition = positions.get(parent); if (parentPosition === undefined) { - // TODO explain that this can never happen + /** + * Since this algorithm populates the `positions` map in level order, + * the parent node will have been processed already and the parent position + * will always be available. + * + * This will never happen. + */ throw new Error(); } + /** + * The x 'offset' is added to the x value of the parent to determine the position of the node. + * We add `parentWidth / -2` in order to align the left side of this node with the left side of its parent. + * We add `numberOfPrecedingSiblings * distanceBetweenNodes` in order to keep each node 1 apart on the x axis. + * We add `runningWidthOfPrecedingSiblings` so that we don't overlap with our preceding siblings. We stack em up. + * We add `width / 2` so that we center the node horizontally (in case it has non-0 width.) + */ const xOffset = parentWidth / -2 + numberOfPrecedingSiblings * distanceBetweenNodes + runningWidthOfPrecedingSiblings + width / 2; + /** + * The y axis gains `-distanceBetweenNodes` as we move down the screen 1 unit at a time. + */ const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition); positions.set(process, position); @@ -289,10 +384,25 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( graphableProcesses /* eslint-enable no-shadow */ ) { + /** + * Index the tree, creating maps from id -> node and id -> children + */ const indexedProcessTree = indexedProcessTreeFactory(graphableProcesses); + /** + * Walk the tree in reverse level order, calculating the 'width' of subtrees. + */ const widths = widthsOfProcessSubtrees(indexedProcessTree); + /** + * Walk the tree in level order. Using the precalculated widths, calculate the position of nodes. + * Nodes are positioned relative to their parents and preceding siblings. + */ const positions = processPositions(indexedProcessTree, widths); + + /** + * With the widths and positions precalculated, we calculate edge line segments (arrays of vector2s) + * which connect them in a 'pitchfork' design. + */ const edgeLineSegments = processEdgeLineSegments(indexedProcessTree, widths, positions); return { From bc840fce8a0725b6b28574f2a86ce5ad5a7ddc75 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 12:12:41 -0500 Subject: [PATCH 74/86] more elite code --- .../resolver/store/data/selectors.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 0d499e1112a19b..1580580f0fc0c0 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -102,18 +102,20 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces return widths; } -// bet sean loves this +/** + * TODO comments + */ type ProcessWithWidthMetadata = { process: ProcessEvent; width: number; } & ( - | ({ + | { parent: ProcessEvent; parentWidth: number; - } & ( // TODO bolete this - | { isOnlyChild: true; firstChildWidth: null; lastChildWidth: null } - | { isOnlyChild: false; firstChildWidth: number; lastChildWidth: number } - )) + isOnlyChild: boolean; + firstChildWidth: number; + lastChildWidth: number; + } | { parent: null; /* Without a parent, there is no parent width */ @@ -272,9 +274,8 @@ function* levelOrderWithWidths( const siblings = indexedProcessTreeChildren(tree, parent); if (siblings.length === 1) { metadata.isOnlyChild = true; - // TODO, just make these === width - metadata.lastChildWidth = null; - metadata.firstChildWidth = null; + metadata.lastChildWidth = width; + metadata.firstChildWidth = width; } else { const firstChildWidth = widths.get(siblings[0]); const lastChildWidth = widths.get(siblings[0]); From 2ae630747fa14941fb05eb25068559be76f445b2 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 12:44:31 -0500 Subject: [PATCH 75/86] more stuff --- .../resolver/models/indexed_process_tree.ts | 3 +- .../resolver/models/process_event.test.ts | 6 ++- .../models/process_event_test_helpers.ts | 29 +++++++++++++ .../embeddables/resolver/store/data/action.ts | 11 ++++- .../resolver/store/data/graphing.test.ts | 42 +++++-------------- .../resolver/store/data/selectors.ts | 2 +- .../public/embeddables/resolver/types.ts | 10 ++++- .../view/use_autoupdating_client_rect.tsx | 5 --- 8 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index 175aea72161582..0eb3505096b4a5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -9,8 +9,7 @@ import { IndexedProcessTree, ProcessEvent } from '../types'; import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; /** - * Create a new `IndexedProcessTree` - * TODO, what is this? + * Create a new IndexedProcessTree from an array of ProcessEvents */ export function factory(processes: ProcessEvent[]): IndexedProcessTree { const idToChildren = new Map(); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts index 201de4032564b8..3177671a30001d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts @@ -5,16 +5,18 @@ */ import { eventType } from './process_event'; import { ProcessEvent } from '../types'; +import { mockProcessEvent } from './process_event_test_helpers'; describe('process event', () => { describe('eventType', () => { let event: ProcessEvent; beforeEach(() => { - event = { + event = mockProcessEvent({ data_buffer: { + node_id: 1, event_type_full: 'process_event', }, - }; + }); }); it("returns the right value when the subType is 'creation_event'", () => { event.data_buffer.event_subtype_full = 'creation_event'; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts new file mode 100644 index 00000000000000..199ca0b15708e6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ProcessEvent } from '../types'; +type DeepPartial = { [K in keyof T]?: DeepPartial }; + +export function mockProcessEvent( + parts: { + data_buffer: { node_id: ProcessEvent['data_buffer']['node_id'] }; + } & DeepPartial +): ProcessEvent { + const { data_buffer: dataBuffer } = parts; + return { + event_timestamp: 1, + event_type: 1, + machine_id: '', + ...parts, + data_buffer: { + event_subtype_full: 'creation_event', + event_type_full: 'process_event', + process_name: '', + process_path: '', + ...dataBuffer, + }, + }; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index a19d5654bcb726..900b9bda571dae 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -4,10 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessEvent } from '../../types'; + interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; - // TODO how dare you - readonly payload: Record; + readonly payload: { + readonly data: { + readonly result: { + readonly search_results: readonly ProcessEvent[]; + }; + }; + }; } export type DataAction = ServerReturnedResolverData; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index d446cc77aaefda..fac70433f14b29 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -9,29 +9,7 @@ import { DataAction } from './action'; import { dataReducer } from './reducer'; import { DataState, ProcessEvent } from '../../types'; import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; - -type DeepPartial = { [K in keyof T]?: DeepPartial }; - -function processEvent( - parts: { - data_buffer: { node_id: ProcessEvent['data_buffer']['node_id'] }; - } & DeepPartial -): ProcessEvent { - const { data_buffer: dataBuffer } = parts; - return { - event_timestamp: 1, - event_type: 1, - machine_id: '', - ...parts, - data_buffer: { - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - process_name: '', - process_path: '', - ...dataBuffer, - }, - }; -} +import { mockProcessEvent } from '../../models/process_event_test_helpers'; describe('resolver graph layout', () => { let processA: ProcessEvent; @@ -58,7 +36,7 @@ describe('resolver graph layout', () => { * H * */ - processA = processEvent({ + processA = mockProcessEvent({ data_buffer: { process_name: '', event_type_full: 'process_event', @@ -66,7 +44,7 @@ describe('resolver graph layout', () => { node_id: 0, }, }); - processB = processEvent({ + processB = mockProcessEvent({ data_buffer: { event_type_full: 'process_event', event_subtype_full: 'already_running', @@ -74,7 +52,7 @@ describe('resolver graph layout', () => { source_id: 0, }, }); - processC = processEvent({ + processC = mockProcessEvent({ data_buffer: { event_type_full: 'process_event', event_subtype_full: 'creation_event', @@ -82,7 +60,7 @@ describe('resolver graph layout', () => { source_id: 0, }, }); - processD = processEvent({ + processD = mockProcessEvent({ data_buffer: { event_type_full: 'process_event', event_subtype_full: 'creation_event', @@ -90,7 +68,7 @@ describe('resolver graph layout', () => { source_id: 1, }, }); - processE = processEvent({ + processE = mockProcessEvent({ data_buffer: { event_type_full: 'process_event', event_subtype_full: 'creation_event', @@ -98,7 +76,7 @@ describe('resolver graph layout', () => { source_id: 1, }, }); - processF = processEvent({ + processF = mockProcessEvent({ data_buffer: { event_type_full: 'process_event', event_subtype_full: 'creation_event', @@ -106,7 +84,7 @@ describe('resolver graph layout', () => { source_id: 2, }, }); - processG = processEvent({ + processG = mockProcessEvent({ data_buffer: { event_type_full: 'process_event', event_subtype_full: 'creation_event', @@ -114,7 +92,7 @@ describe('resolver graph layout', () => { source_id: 2, }, }); - processH = processEvent({ + processH = mockProcessEvent({ data_buffer: { event_type_full: 'process_event', event_subtype_full: 'creation_event', @@ -122,7 +100,7 @@ describe('resolver graph layout', () => { source_id: 6, }, }); - processI = processEvent({ + processI = mockProcessEvent({ data_buffer: { event_type_full: 'process_event', event_subtype_full: 'termination_event', diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 1580580f0fc0c0..fa5b48a8cc0268 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -103,7 +103,7 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces } /** - * TODO comments + * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. */ type ProcessWithWidthMetadata = { process: ProcessEvent; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 19107d9a5e2162..5e6d3b60138942 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -65,7 +65,7 @@ export interface CameraState { } export interface DataState { - readonly results: ProcessEvent[]; + readonly results: readonly ProcessEvent[]; } export type Vector2 = readonly [number, number]; @@ -124,11 +124,17 @@ export interface ProcessEvent { }; } +/** + * A represention of a process tree with indices for O(1) access to children and values by id. + */ export interface IndexedProcessTree { /** - * `id` can be undefined because `source_id` can be undefined. Root nodes have no known parent + * Map of ID to a process's children */ idToChildren: Map; + /** + * Map of ID to process + */ idToProcess: Map; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx index 54bc8ad1521632..5f13995de1c2ad 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx @@ -16,11 +16,6 @@ import ResizeObserver from 'resize-observer-polyfill'; * Note that the changes to the position of the element aren't automatically * tracked. So if the element's position moves for some reason, be sure to * handle that. - * - * Future performance improvement ideas: - * If `getBoundingClientRect` calls are happening frequently and this is causing performance issues - * we could call getBoundingClientRect less and instead invalidate a cached version of it. - * When needed, we could call `getBoundingClientRect` and store it. TODO more deets */ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { const [rect, setRect] = useState(null); From d78c475bd231e92511178d1001527b31a42fb004 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 12:54:38 -0500 Subject: [PATCH 76/86] just about good --- .../resolver/store/data/selectors.ts | 40 ++++------------ .../public/embeddables/resolver/types.ts | 48 +++++++++++++++++-- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index fa5b48a8cc0268..8e55fe5b390ba0 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -5,7 +5,15 @@ */ import { createSelector } from 'reselect'; -import { DataState, ProcessEvent, IndexedProcessTree } from '../../types'; +import { + DataState, + ProcessEvent, + IndexedProcessTree, + ProcessWidths, + ProcessPositions, + EdgeLineSegment, + ProcessWithWidthMetadata, +} from '../../types'; import { Vector2 } from '../../types'; import { add as vector2Add } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; @@ -17,10 +25,6 @@ import { levelOrder, } from '../../models/indexed_process_tree'; -type ProcessWidths = Map; -type ProcessPositions = Map; -type EdgeLineSegment = Vector2[]; - const unit = 100; const distanceBetweenNodesInUnits = 1; @@ -102,32 +106,6 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces return widths; } -/** - * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. - */ -type ProcessWithWidthMetadata = { - process: ProcessEvent; - width: number; -} & ( - | { - parent: ProcessEvent; - parentWidth: number; - isOnlyChild: boolean; - firstChildWidth: number; - lastChildWidth: number; - } - | { - parent: null; - /* Without a parent, there is no parent width */ - parentWidth: null; - /* Without a parent, we can't be an only child */ - isOnlyChild: null; - /** If there is no parent, there are no siblings */ - lastChildWidth: null; - firstChildWidth: null; - } -); - function processEdgeLineSegments( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 5e6d3b60138942..ed7eb79d621fcc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -64,6 +64,9 @@ export interface CameraState { readonly latestFocusedWorldCoordinates: Vector2 | null; } +/** + * State for `data` reducer which handles receiving Resolver data from the backend. + */ export interface DataState { readonly results: readonly ProcessEvent[]; } @@ -110,6 +113,9 @@ type eventSubtypeFull = type eventTypeFull = 'process_event'; +/** + * The 'events' which contain process data and are used to model Resolver. + */ export interface ProcessEvent { readonly event_timestamp: number; readonly event_type: number; @@ -138,7 +144,41 @@ export interface IndexedProcessTree { idToProcess: Map; } -export interface GraphableProcessesPidMaps { - processesByUniqueParentPid: Map; - processesByUniquePid: Map; -} +/** + * A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` + */ +export type ProcessWidths = Map; +/** + * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` + */ +export type ProcessPositions = Map; +/** + * An array of vectors2 forming an polyline. Used to connect process nodes in the graph. + */ +export type EdgeLineSegment = Vector2[]; + +/** + * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. + */ +export type ProcessWithWidthMetadata = { + process: ProcessEvent; + width: number; +} & ( + | { + parent: ProcessEvent; + parentWidth: number; + isOnlyChild: boolean; + firstChildWidth: number; + lastChildWidth: number; + } + | { + parent: null; + /* Without a parent, there is no parent width */ + parentWidth: null; + /* Without a parent, we can't be an only child */ + isOnlyChild: null; + /** If there is no parent, there are no siblings */ + lastChildWidth: null; + firstChildWidth: null; + } +); From 5f37888f541bcbc6b898433fbbf67a14b1008377 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 13:51:44 -0500 Subject: [PATCH 77/86] we must never speak of this --- .../embeddables/resolver/lib/vector2.ts | 2 +- .../resolver/store/camera/selectors.ts | 20 +++++++++++-------- .../resolver/store/data/selectors.ts | 10 +++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts index 6009b6879a6312..3c0681413305e2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts @@ -30,7 +30,7 @@ export function divide(a: Vector2, b: Vector2): Vector2 { * Returns a vector which is the result of applying a 2D transformation matrix to the provided vector. */ export function applyMatrix3([x, y]: Vector2, [m11, m12, m13, m21, m22, m23]: Matrix3): Vector2 { - return [x * m11 + y * m12 + m13, y * m21 + y * m22 + m23]; + return [x * m11 + y * m12 + m13, x * m21 + y * m22 + m23]; } /** diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index ca62d7e012976e..e0d2062bfc8706 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -29,9 +29,11 @@ interface ClippingPlanes { export function viewableBoundingBox(state: CameraState): AABB { const { renderWidth, renderHeight } = clippingPlanes(state); const matrix = inverseProjectionMatrix(state); + const bottomLeftCorner: Vector2 = [0, renderHeight]; + const topRightCorner: Vector2 = [renderWidth, 0]; return { - minimum: applyMatrix3([0, renderHeight], matrix), - maximum: applyMatrix3([renderWidth, 0], matrix), + minimum: applyMatrix3(bottomLeftCorner, matrix), + maximum: applyMatrix3(topRightCorner, matrix), }; } @@ -134,6 +136,13 @@ export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => clippingPlaneBottom, } = clippingPlanes(state); + /* prettier-ignore */ + const screenToNDC = [ + 2 / renderWidth, 0, -1, + 0, 2 / renderHeight, -1, + 0, 0, 0 + ] as const + const [translationX, translationY] = translation(state); return addMatrix( @@ -157,12 +166,7 @@ export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => scalingTransformation([1, -1]), // 1. convert screen coordinates to NDC // e.g. for x-axis, divide by renderWidth then multiply by 2 and subtract by one so the value is in range of -1->1 - // prettier-ignore - [ - 2 / renderWidth, 0, -1, - 2 / renderHeight, 0, -1, - 0, 0, 0 - ] as const + screenToNDC ) ) ); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 8e55fe5b390ba0..cbdcef13e9005a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -15,7 +15,7 @@ import { ProcessWithWidthMetadata, } from '../../types'; import { Vector2 } from '../../types'; -import { add as vector2Add } from '../../lib/vector2'; +import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; import { factory as indexedProcessTreeFactory, @@ -346,6 +346,14 @@ function processPositions( */ const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition); + /* prettier-ignore */ + const isometricTransformMatrix = [ + Math.sqrt(2) / 2, -(Math.sqrt(2) / 2), 0, + Math.sqrt(6) / 6, Math.sqrt(6) / 6, -(Math.sqrt(6) / 3), + 0, 0, 1, + ] + + // positions.set(process, applyMatrix3(position, isometricTransformMatrix)); positions.set(process, position); numberOfPrecedingSiblings += 1; From 5112ed79e0941caea263c1222451c445ff9da3f1 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 14:07:54 -0500 Subject: [PATCH 78/86] get isometric --- .../resolver/lib/transformation.ts | 9 +++++ .../resolver/store/camera/reducer.ts | 2 +- .../resolver/store/data/selectors.ts | 39 ++++++++++++++----- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts index 3084ce0eacdb4c..efedda52ce7511 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts @@ -71,3 +71,12 @@ export function translationTransformation([x, y]: Vector2): Matrix3 { 0, 0, 1 ] } + +export function rotationTransformation(angleInRadians: number): Matrix3 { + // prettier-ignore + return [ + Math.cos(angleInRadians), -Math.sin(angleInRadians), 0, + Math.sin(angleInRadians), Math.cos(angleInRadians), 0, + 0, 0, 1 + ] +} 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 b4e05b8521e604..457d3904804f2a 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 @@ -28,7 +28,7 @@ const minimumScale = 0.1; /** * The maximum allowed value for the camera scale. This is greatest scale that we will ever render something at. */ -const maximumScale = 3; +const maximumScale = 6; export const cameraReducer: Reducer = ( state = initialState(), diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index cbdcef13e9005a..a0de2537ab1ab3 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -5,6 +5,7 @@ */ import { createSelector } from 'reselect'; +import { rotationTransformation } from '../../lib/transformation'; import { DataState, ProcessEvent, @@ -13,6 +14,7 @@ import { ProcessPositions, EdgeLineSegment, ProcessWithWidthMetadata, + Matrix3, } from '../../types'; import { Vector2 } from '../../types'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; @@ -28,6 +30,13 @@ import { const unit = 100; const distanceBetweenNodesInUnits = 1; +/* prettier-ignore */ +const isometricTransformMatrix: Matrix3 = [ + Math.sqrt(2) / 2, -(Math.sqrt(2) / 2), 0, + Math.sqrt(6) / 6, Math.sqrt(6) / 6, -(Math.sqrt(6) / 3), + 0, 0, 1, +] + /** * The distance in pixels (at scale 1) between nodes. Change this to space out nodes more */ @@ -346,14 +355,6 @@ function processPositions( */ const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition); - /* prettier-ignore */ - const isometricTransformMatrix = [ - Math.sqrt(2) / 2, -(Math.sqrt(2) / 2), 0, - Math.sqrt(6) / 6, Math.sqrt(6) / 6, -(Math.sqrt(6) / 3), - 0, 0, 1, - ] - - // positions.set(process, applyMatrix3(position, isometricTransformMatrix)); positions.set(process, position); numberOfPrecedingSiblings += 1; @@ -392,9 +393,27 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( */ const edgeLineSegments = processEdgeLineSegments(indexedProcessTree, widths, positions); + /** + * Transform the positions of nodes and edges so they seem like they are on an isometric grid. + */ + const transformedEdgeLineSegments: EdgeLineSegment[] = []; + const transformedPositions = new Map(); + + for (const [processEvent, position] of positions) { + transformedPositions.set(processEvent, applyMatrix3(position, isometricTransformMatrix)); + } + + for (const edgeLineSegment of edgeLineSegments) { + const transformedSegment = []; + for (const point of edgeLineSegment) { + transformedSegment.push(applyMatrix3(point, isometricTransformMatrix)); + } + transformedEdgeLineSegments.push(transformedSegment); + } + return { - processNodePositions: positions, - edgeLineSegments, + processNodePositions: transformedPositions, + edgeLineSegments: transformedEdgeLineSegments, }; } ); From 1ebcddce32ac25c76a7a0bd4d4de5c3aefa9f742 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 14:47:37 -0500 Subject: [PATCH 79/86] remove todo --- .../resolver_test/public/applications/resolver_test/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index f58aad813f3e96..d9b319e9b93fb3 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -19,7 +19,6 @@ export function renderApp( { element }: AppMountParameters, embeddable: Promise ) { - // TODO, is this right? element.style.display = 'flex'; element.style.flexGrow = '1'; From 1fef3b778efc999c9f0e174e5340a301dded3e74 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 14:51:42 -0500 Subject: [PATCH 80/86] update snapshots to match isometric stuff --- .../data/__snapshots__/graphing.test.ts.snap | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap index 40b0b8715cfa53..261ca7e0a7bbaf 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -24,7 +24,7 @@ Object { "machine_id": "", } => Array [ 0, - 0, + -0.8164965809277259, ], }, } @@ -36,131 +36,131 @@ Object { Array [ Array [ 0, - 0, + -0.8164965809277259, ], Array [ - 0, - -50, + 35.35533905932738, + -21.228911104120876, ], ], Array [ Array [ - -100, - -50, + -35.35533905932738, + -62.053740150507174, ], Array [ - 100, - -50, + 106.06601717798213, + 19.595917942265423, ], ], Array [ Array [ - -100, - -50, + -35.35533905932738, + -62.053740150507174, ], Array [ - -100, - -100, + 0, + -82.46615467370032, ], ], Array [ Array [ - 100, - -50, + 106.06601717798213, + 19.595917942265423, ], Array [ - 100, - -100, + 141.4213562373095, + -0.8164965809277259, ], ], Array [ Array [ - -100, - -100, + 0, + -82.46615467370032, ], Array [ - -100, - -150, + 35.35533905932738, + -102.87856919689347, ], ], Array [ Array [ - -150, - -150, + 0, + -123.2909837200866, ], Array [ - -50, - -150, + 70.71067811865476, + -82.46615467370032, ], ], Array [ Array [ - -150, - -150, + 0, + -123.2909837200866, ], Array [ - -150, - -200, + 35.35533905932738, + -143.70339824327976, ], ], Array [ Array [ - -50, - -150, + 70.71067811865476, + -82.46615467370032, ], Array [ - -50, - -200, + 106.06601717798213, + -102.87856919689347, ], ], Array [ Array [ - 100, - -100, + 141.4213562373095, + -0.8164965809277259, ], Array [ - 100, - -150, + 176.7766952966369, + -21.22891110412087, ], ], Array [ Array [ - 50, - -150, + 141.4213562373095, + -41.64132562731402, ], Array [ - 150, - -150, + 212.13203435596427, + -0.8164965809277259, ], ], Array [ Array [ - 50, - -150, + 141.4213562373095, + -41.64132562731402, ], Array [ - 50, - -200, + 176.7766952966369, + -62.053740150507174, ], ], Array [ Array [ - 150, - -150, + 212.13203435596427, + -0.8164965809277259, ], Array [ - 150, - -200, + 247.48737341529164, + -21.228911104120883, ], ], Array [ Array [ - 150, - -200, + 247.48737341529164, + -21.228911104120883, ], Array [ - 150, - -300, + 318.1980515339464, + -62.05374015050717, ], ], ], @@ -178,7 +178,7 @@ Object { "machine_id": "", } => Array [ 0, - 0, + -0.8164965809277259, ], Object { "data_buffer": Object { @@ -193,8 +193,8 @@ Object { "event_type": 1, "machine_id": "", } => Array [ - -100, - -100, + 0, + -82.46615467370032, ], Object { "data_buffer": Object { @@ -209,8 +209,8 @@ Object { "event_type": 1, "machine_id": "", } => Array [ - 100, - -100, + 141.4213562373095, + -0.8164965809277259, ], Object { "data_buffer": Object { @@ -225,8 +225,8 @@ Object { "event_type": 1, "machine_id": "", } => Array [ - -150, - -200, + 35.35533905932738, + -143.70339824327976, ], Object { "data_buffer": Object { @@ -241,8 +241,8 @@ Object { "event_type": 1, "machine_id": "", } => Array [ - -50, - -200, + 106.06601717798213, + -102.87856919689347, ], Object { "data_buffer": Object { @@ -257,8 +257,8 @@ Object { "event_type": 1, "machine_id": "", } => Array [ - 50, - -200, + 176.7766952966369, + -62.053740150507174, ], Object { "data_buffer": Object { @@ -273,8 +273,8 @@ Object { "event_type": 1, "machine_id": "", } => Array [ - 150, - -200, + 247.48737341529164, + -21.228911104120883, ], Object { "data_buffer": Object { @@ -289,8 +289,8 @@ Object { "event_type": 1, "machine_id": "", } => Array [ - 150, - -300, + 318.1980515339464, + -62.05374015050717, ], }, } @@ -302,11 +302,11 @@ Object { Array [ Array [ 0, - 0, + -0.8164965809277259, ], Array [ - 0, - -100, + 70.71067811865476, + -41.641325627314025, ], ], ], @@ -324,7 +324,7 @@ Object { "machine_id": "", } => Array [ 0, - 0, + -0.8164965809277259, ], Object { "data_buffer": Object { @@ -339,8 +339,8 @@ Object { "event_type": 1, "machine_id": "", } => Array [ - 0, - -100, + 70.71067811865476, + -41.641325627314025, ], }, } From 679fa35f6ea515132a914abfa588a2acba8290d2 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Thu, 9 Jan 2020 14:55:56 -0500 Subject: [PATCH 81/86] remove unused function --- .../public/embeddables/resolver/lib/transformation.ts | 9 --------- .../public/embeddables/resolver/store/data/selectors.ts | 1 - 2 files changed, 10 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts index efedda52ce7511..3084ce0eacdb4c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts @@ -71,12 +71,3 @@ export function translationTransformation([x, y]: Vector2): Matrix3 { 0, 0, 1 ] } - -export function rotationTransformation(angleInRadians: number): Matrix3 { - // prettier-ignore - return [ - Math.cos(angleInRadians), -Math.sin(angleInRadians), 0, - Math.sin(angleInRadians), Math.cos(angleInRadians), 0, - 0, 0, 1 - ] -} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index a0de2537ab1ab3..10cb6e0c90abad 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -5,7 +5,6 @@ */ import { createSelector } from 'reselect'; -import { rotationTransformation } from '../../lib/transformation'; import { DataState, ProcessEvent, From 57fa7f9a44b72d8906ab613b096dc17b863fe4a2 Mon Sep 17 00:00:00 2001 From: Pedro Jaramillo Date: Fri, 10 Jan 2020 13:34:58 -0500 Subject: [PATCH 82/86] add some comments --- .../resolver/models/process_event.ts | 13 +++++++++++++ .../models/process_event_test_helpers.ts | 6 ++++++ .../resolver/store/data/selectors.ts | 17 +++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts index 5dcf7ece8808d6..37979ffd75b611 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts @@ -6,10 +6,17 @@ import { ProcessEvent } from '../types'; +/* + * Returns true is the process's eventType is either 'processCreated' or 'processRan'. + * Resolver will only render 'graphable' process events. + */ export function isGraphableProcess(event: ProcessEvent) { return eventType(event) === 'processCreated' || eventType(event) === 'processRan'; } +/* + * Returns a custom event type for a process event based on the event's metadata. + */ export function eventType(event: ProcessEvent) { const { data_buffer: { event_type_full: type, event_subtype_full: subType }, @@ -31,10 +38,16 @@ export function eventType(event: ProcessEvent) { return 'unknownEvent'; } +/* + * Returns the process event's pid + */ export function uniquePidForProcess(event: ProcessEvent) { return event.data_buffer.node_id; } +/* + * Returns the process event's parent pid + */ export function uniqueParentPidForProcess(event: ProcessEvent) { return event.data_buffer.source_id; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts index 199ca0b15708e6..3306f489abe847 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts @@ -7,6 +7,12 @@ import { ProcessEvent } from '../types'; type DeepPartial = { [K in keyof T]?: DeepPartial }; +/* + * Creates a mock process event given the 'parts' argument, which can + * include all or some process event fields as determined by the ProcessEvent type. + * The only field that must be provided is the event's 'node_id' field. + * The other fields are populated by the function unless provided in 'parts' + */ export function mockProcessEvent( parts: { data_buffer: { node_id: ProcessEvent['data_buffer']['node_id'] }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 10cb6e0c90abad..c94aa74806bc77 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -29,6 +29,23 @@ import { const unit = 100; const distanceBetweenNodesInUnits = 1; +/* An isometric projection is a method for representing three dimensional objects in 2 dimensions. + * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection. + * In our case, we obtain the isometric projection by rotating the objects 45 degrees in the plane of the screen + * and arctan(1/sqrt(2)) (~35.3 degrees) through the horizontal axis. + * + * A rotation by 45 degrees in the plane of the screen is given by: + * [ sqrt(2)/2 -sqrt(2)/2 0 + * sqrt(2)/2 sqrt(2)/2 0 + * 0 0 1] + * + * A rotation by arctan(1/sqrt(2)) through the horizantal axis is given by: + * [ 1 0 0 + * 0 sqrt(3)/3 -sqrt(6)/3 + * 0 sqrt(6)/3 sqrt(3)/3] + * + * We can multiply both of these matrices to get the final transformation below. + */ /* prettier-ignore */ const isometricTransformMatrix: Matrix3 = [ Math.sqrt(2) / 2, -(Math.sqrt(2) / 2), 0, From 2462aa288a77e25d62f84ae23e7b4be01aef7f1c Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 13 Jan 2020 15:21:29 -0500 Subject: [PATCH 83/86] more commints --- .../embeddables/resolver/view/edge_line.tsx | 30 ++++++++++++++++ .../embeddables/resolver/view/index.tsx | 5 ++- .../resolver/view/process_event_dot.tsx | 34 ++++++++++++++----- .../applications/resolver_test/index.tsx | 3 ++ .../plugins/resolver_test/public/plugin.ts | 9 +++++ 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx index 48138e1ef69496..cdecd3e02bde10 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx @@ -11,6 +11,9 @@ import { applyMatrix3, distance, angle } from '../lib/vector2'; import { Vector2 } from '../types'; import * as selectors from '../store/selectors'; +/** + * A placeholder line segment view that connects process nodes. + */ export const EdgeLine = styled( React.memo( ({ @@ -18,20 +21,47 @@ export const EdgeLine = styled( startPosition, endPosition, }: { + /** + * A className string provided by `styled` + */ className?: string; + /** + * The postion of first point in the line segment. In 'world' coordinates. + */ startPosition: Vector2; + /** + * The postion of second point in the line segment. In 'world' coordinates. + */ endPosition: Vector2; }) => { + /** + * Convert the start and end positions, which are in 'world' coordinates, + * to `left` and `top` css values. + */ const projectionMatrix = useSelector(selectors.projectionMatrix); const screenStart = applyMatrix3(startPosition, projectionMatrix); const screenEnd = applyMatrix3(endPosition, projectionMatrix); + + /** + * We render the line using a short, long, `div` element. The length of this `div` + * should be the same as the distance between the start and end points. + */ const length = distance(screenStart, screenEnd); const style = { left: screenStart[0] + 'px', top: screenStart[1] + 'px', width: length + 'px', + /** + * Transform from the left of the div, as the left side of the `div` is positioned + * at the start point of the line segment. + */ transformOrigin: 'top left', + /** + * Translate the `div` in the y axis to accomodate for the height of the `div`. + * Also rotate the `div` in the z axis so that it's angle matches the angle + * between the start and end points. + */ transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`, }; 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 1c8efc3aeeb3c7..2c5c60440522dd 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -144,7 +144,7 @@ const Resolver = styled( onMouseDown={handleMouseDown} > {Array.from(processNodePositions).map(([processEvent, position], index) => ( - + ))} {edgeLineSegments.map(([startPosition, endPosition], index) => ( @@ -153,6 +153,9 @@ const Resolver = styled( ); }) )` + /** + * Take up all availble space + */ display: flex; flex-grow: 1; position: relative; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 1d7edf429f911a..5c3a253d619ef8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -11,30 +11,45 @@ import { applyMatrix3 } from '../lib/vector2'; import { Vector2, ProcessEvent } from '../types'; import * as selectors from '../store/selectors'; +/** + * A placeholder view for a process node. + */ export const ProcessEventDot = styled( React.memo( ({ className, - worldPosition, - processEvent, + position, + event, }: { + /** + * A `className` string provided by `styled` + */ className?: string; - worldPosition: Vector2; - processEvent: ProcessEvent; + /** + * The positon of the process node, in 'world' coordinates. + */ + position: Vector2; + /** + * An event which contains details about the process node. + */ + event: ProcessEvent; }) => { + /** + * Convert the position, which is in 'world' coordinates, to screen coordinates. + */ const projectionMatrix = useSelector(selectors.projectionMatrix); - const [left, top] = applyMatrix3(worldPosition, projectionMatrix); + const [left, top] = applyMatrix3(position, projectionMatrix); const style = { left: (left - 20).toString() + 'px', top: (top - 20).toString() + 'px', }; return ( - name: {processEvent.data_buffer.process_name} + name: {event.data_buffer.process_name}
- x: {worldPosition[0]} + x: {position[0]}
- y: {worldPosition[1]} + y: {position[1]}
); } @@ -45,6 +60,9 @@ export const ProcessEventDot = styled( height: 40px; text-align: left; font-size: 10px; + /** + * Give the element a button-like appearance. + */ user-select: none; border: 1px solid black; box-sizing: border-box; diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index d9b319e9b93fb3..19b595d884ce53 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -19,6 +19,9 @@ export function renderApp( { element }: AppMountParameters, embeddable: Promise ) { + /** + * The application DOM node should take all available space. + */ element.style.display = 'flex'; element.style.flexGrow = '1'; diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts index f063271f4b5ddb..6d9f3d213c0323 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -26,6 +26,12 @@ export class ResolverTestPlugin private resolveEmbeddable!: ( value: IEmbeddable | undefined | PromiseLike | undefined ) => void; + /** + * We register our application during the `setup` phase, but the embeddable + * plugin API is not available until the `start` phase. In order to access + * the embeddable API from our application, we pass a Promise to the application + * which we resolve during the `start` phase. + */ private embeddablePromise: Promise = new Promise< IEmbeddable | undefined >(resolve => { @@ -47,6 +53,9 @@ export class ResolverTestPlugin public start(...args: [unknown, { embeddable: IEmbeddableStart }]) { const [, plugins] = args; const factory = plugins.embeddable.getEmbeddableFactory('resolver'); + /** + * Provide the Resolver embeddable to the application + */ this.resolveEmbeddable(factory.create({ id: 'test basic render' })); } public stop() {} From 015c110c9690ed007ea0a64dd30ebe64650362ad Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 13 Jan 2020 15:30:12 -0500 Subject: [PATCH 84/86] do comments right --- .../embeddables/resolver/models/process_event.ts | 10 +++++----- .../resolver/models/process_event_test_helpers.ts | 2 +- .../embeddables/resolver/store/data/selectors.ts | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts index 37979ffd75b611..c8496b8e6e7a5d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts @@ -6,15 +6,15 @@ import { ProcessEvent } from '../types'; -/* - * Returns true is the process's eventType is either 'processCreated' or 'processRan'. +/** + * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ export function isGraphableProcess(event: ProcessEvent) { return eventType(event) === 'processCreated' || eventType(event) === 'processRan'; } -/* +/** * Returns a custom event type for a process event based on the event's metadata. */ export function eventType(event: ProcessEvent) { @@ -38,14 +38,14 @@ export function eventType(event: ProcessEvent) { return 'unknownEvent'; } -/* +/** * Returns the process event's pid */ export function uniquePidForProcess(event: ProcessEvent) { return event.data_buffer.node_id; } -/* +/** * Returns the process event's parent pid */ export function uniqueParentPidForProcess(event: ProcessEvent) { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts index 3306f489abe847..67acdbd253f65a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts @@ -7,7 +7,7 @@ import { ProcessEvent } from '../types'; type DeepPartial = { [K in keyof T]?: DeepPartial }; -/* +/** * Creates a mock process event given the 'parts' argument, which can * include all or some process event fields as determined by the ProcessEvent type. * The only field that must be provided is the event's 'node_id' field. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index c94aa74806bc77..745bd125c151d2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -29,7 +29,8 @@ import { const unit = 100; const distanceBetweenNodesInUnits = 1; -/* An isometric projection is a method for representing three dimensional objects in 2 dimensions. +/** + * An isometric projection is a method for representing three dimensional objects in 2 dimensions. * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection. * In our case, we obtain the isometric projection by rotating the objects 45 degrees in the plane of the screen * and arctan(1/sqrt(2)) (~35.3 degrees) through the horizontal axis. From ce7880b620db3c8846d8408df0569388f60f3b94 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 13 Jan 2020 15:52:20 -0500 Subject: [PATCH 85/86] comments --- .../applications/resolver_test/index.tsx | 50 ++++++++++++++++--- .../plugins/resolver_test/public/plugin.ts | 3 ++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index 19b595d884ce53..55fd436de40a15 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -43,29 +43,64 @@ const AppRoot = styled( embeddable: embeddablePromise, className, }: { + /** + * A promise which resolves to the Resolver embeddable. + */ embeddable: Promise; + /** + * A `className` string provided by `styled` + */ className?: string; }) => { + /** + * This state holds the reference to the embeddable, once resolved. + */ const [embeddable, setEmbeddable] = React.useState(undefined); + /** + * This state holds the reference to the DOM node that will contain the embeddable. + */ const [renderTarget, setRenderTarget] = React.useState(null); + /** + * Keep component state with the Resolver embeddable. + * + * If the reference to the embeddablePromise changes, we ignore the stale promise. + */ useEffect(() => { + /** + * A promise rejection function that will prevent a stale embeddable promise from being resolved + * as the current eembeddable. + * + * If the embeddablePromise itself changes before the old one is resolved, we cancel and restart this effect. + */ let cleanUp; - Promise.race([ - new Promise((_resolve, reject) => { - cleanUp = reject; - }), - embeddablePromise, - ]).then(value => { + + const cleanupPromise = new Promise((_resolve, reject) => { + cleanUp = reject; + }); + + /** + * Either set the embeddable in state, or cancel and restart this process. + */ + Promise.race([cleanupPromise, embeddablePromise]).then(value => { setEmbeddable(value); }); + /** + * If `embeddablePromise` is changed, the cleanup function is run. + */ return cleanUp; }, [embeddablePromise]); + /** + * Render the eembeddable into the DOM node. + */ useEffect(() => { if (embeddable && renderTarget) { embeddable.render(renderTarget); + /** + * If the embeddable or DOM node changes then destroy the old embeddable. + */ return () => { embeddable.destroy(); }; @@ -82,6 +117,9 @@ const AppRoot = styled( } ) )` + /** + * Take all available space. + */ display: flex; flex-grow: 1; `; diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts index 6d9f3d213c0323..9252825ea11072 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -45,6 +45,9 @@ export class ResolverTestPlugin }), mount: async (_context, params) => { const { renderApp } = await import('./applications/resolver_test'); + /** + * Pass a promise which resolves to the Resolver embeddable. + */ return renderApp(params, this.embeddablePromise); }, }); From b5df6466d209e2a796c3374e1b63d2857625e047 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 14 Jan 2020 08:21:49 -0500 Subject: [PATCH 86/86] remove endpoint workspace --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 20738ab725a6c3..546a19c6eaba50 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,6 @@ "x-pack", "x-pack/plugins/*", "x-pack/legacy/plugins/*", - "x-pack/plugins/endpoint", "examples/*", "test/plugin_functional/plugins/*", "test/interpreter_functional/plugins/*"