From 6f0a90b840c40ee8c714ce259bb8112812f60f77 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Sun, 19 Jul 2020 00:54:02 -0400 Subject: [PATCH 1/8] Center the origin process. also renames 'isProcessOrigin' params in resolver theme to be 'isProcessTrigger'. NB: this does not use the trigger node styling --- .../isometric_taxi_layout.test.ts | 4 +- .../isometric_taxi_layout.ts | 56 +++++++++++++---- .../public/resolver/store/data/selectors.ts | 63 ++++++++++++++++--- .../public/resolver/store/selectors.ts | 8 +++ .../public/resolver/view/assets.tsx | 12 ++-- .../public/resolver/view/map.tsx | 1 - .../resolver/view/process_event_dot.tsx | 7 +-- 7 files changed, 118 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts index bd534dcb989e3..40be175c9fdbb 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts @@ -5,7 +5,7 @@ */ import { IsometricTaxiLayout } from '../../types'; import { LegacyEndpointEvent } from '../../../../common/endpoint/types'; -import { isometricTaxiLayout } from './isometric_taxi_layout'; +import { isometricTaxiLayoutFactory } from './isometric_taxi_layout'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; import { factory } from './index'; @@ -107,7 +107,7 @@ describe('resolver graph layout', () => { unique_ppid: 0, }, }); - layout = () => isometricTaxiLayout(factory(events)); + layout = () => isometricTaxiLayoutFactory(factory(events)); events = []; }); describe('when rendering no nodes', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts index 11c888d1462f8..a9f3c46e375a8 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts @@ -3,7 +3,6 @@ * 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 vector2 from '../../models/vector2'; import { IndexedProcessTree, Vector2, @@ -17,14 +16,17 @@ import { } from '../../types'; import * as event from '../../../../common/endpoint/models/event'; import { ResolverEvent } from '../../../../common/endpoint/types'; -import * as model from './index'; +import * as vector2 from '../vector2'; +import * as indexedProcessTreeModel from './index'; import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date'; import { uniquePidForProcess } from '../process_event'; /** * Graph the process tree */ -export function isometricTaxiLayout(indexedProcessTree: IndexedProcessTree): IsometricTaxiLayout { +export function isometricTaxiLayoutFactory( + indexedProcessTree: IndexedProcessTree +): IsometricTaxiLayout { /** * Walk the tree in reverse level order, calculating the 'width' of subtrees. */ @@ -83,8 +85,8 @@ export function isometricTaxiLayout(indexedProcessTree: IndexedProcessTree): Iso */ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map { const map: Map = new Map(); - for (const node of model.levelOrder(indexedProcessTree)) { - const parentNode = model.parent(indexedProcessTree, node); + for (const node of indexedProcessTreeModel.levelOrder(indexedProcessTree)) { + const parentNode = indexedProcessTreeModel.parent(indexedProcessTree, node); if (parentNode === undefined) { // nodes at the root have a level of 1 map.set(node, 1); @@ -143,16 +145,19 @@ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map(); - if (model.size(indexedProcessTree) === 0) { + if (indexedProcessTreeModel.size(indexedProcessTree) === 0) { return widths; } const processesInReverseLevelOrder: ResolverEvent[] = [ - ...model.levelOrder(indexedProcessTree), + ...indexedProcessTreeModel.levelOrder(indexedProcessTree), ].reverse(); for (const process of processesInReverseLevelOrder) { - const children = model.children(indexedProcessTree, uniquePidForProcess(process)); + const children = indexedProcessTreeModel.children( + indexedProcessTree, + uniquePidForProcess(process) + ); const sumOfWidthOfChildren = function sumOfWidthOfChildren() { return children.reduce(function sum(currentValue, child) { @@ -229,7 +234,10 @@ function processEdgeLineSegments( metadata: edgeLineMetadata, }; - const siblings = model.children(indexedProcessTree, uniquePidForProcess(parent)); + const siblings = indexedProcessTreeModel.children( + indexedProcessTree, + uniquePidForProcess(parent) + ); const isFirstChild = process === siblings[0]; if (metadata.isOnlyChild) { @@ -384,8 +392,8 @@ function* levelOrderWithWidths( tree: IndexedProcessTree, widths: ProcessWidths ): Iterable { - for (const process of model.levelOrder(tree)) { - const parent = model.parent(tree, process); + for (const process of indexedProcessTreeModel.levelOrder(tree)) { + const parent = indexedProcessTreeModel.parent(tree, process); const width = widths.get(process); if (width === undefined) { @@ -423,7 +431,7 @@ function* levelOrderWithWidths( parentWidth, }; - const siblings = model.children(tree, uniquePidForProcess(parent)); + const siblings = indexedProcessTreeModel.children(tree, uniquePidForProcess(parent)); if (siblings.length === 1) { metadata.isOnlyChild = true; metadata.lastChildWidth = width; @@ -479,3 +487,27 @@ const distanceBetweenNodesInUnits = 2; * The distance in pixels (at scale 1) between nodes. Change this to space out nodes more */ const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; + +export function nodePosition(model: IsometricTaxiLayout, node: ResolverEvent): Vector2 | undefined { + return model.processNodePositions.get(node); +} + +/** + * Return a clone of `model` with all positions incremented by `position`. + */ +export function translated(model: IsometricTaxiLayout, translation: Vector2): IsometricTaxiLayout { + return { + processNodePositions: new Map( + [...model.processNodePositions.entries()].map(([node, position]) => [ + node, + vector2.add(position, translation), + ]) + ), + edgeLineSegments: model.edgeLineSegments.map(({ points, metadata }) => ({ + points: points.map((point) => vector2.add(point, translation)), + metadata, + })), + // these are unchanged + ariaLevels: model.ariaLevels, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 4098c6fc6c5dd..bb736e9e98b5c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -30,8 +30,9 @@ import { ResolverRelatedEvents, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; -import { isometricTaxiLayout } from '../../models/indexed_process_tree/isometric_taxi_layout'; +import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; import { allEventCategories } from '../../../../common/endpoint/models/event'; +import * as vector2 from '../../models/vector2'; /** * If there is currently a request. @@ -70,6 +71,21 @@ const resolverTreeResponse = (state: DataState): ResolverTree | undefined => { } }; +/** + * the node ID of the node representing the databaseDocumentID. + * NB: this could be stale if the last response is stale + */ +export const originID: (state: DataState) => string | undefined = createSelector( + resolverTreeResponse, + function (resolverTree?) { + if (resolverTree) { + // This holds the entityID (aka nodeID) of the node related to the last fetched `_id` + return resolverTree.entityID; + } + return undefined; + } +); + /** * Process events that will be displayed as terminated. */ @@ -317,13 +333,44 @@ export function databaseDocumentIDToFetch(state: DataState): string | null { } } -export const layout = createSelector(tree, function processNodePositionsAndEdgeLineSegments( - /* eslint-disable no-shadow */ - indexedProcessTree - /* eslint-enable no-shadow */ -) { - return isometricTaxiLayout(indexedProcessTree); -}); +export const layout = createSelector( + tree, + originID, + function processNodePositionsAndEdgeLineSegments( + /* eslint-disable no-shadow */ + indexedProcessTree, + originID + /* eslint-enable no-shadow */ + ) { + // use the isometric taxi layout as a base + const taxiLayout = isometricTaxiLayoutModel.isometricTaxiLayoutFactory(indexedProcessTree); + + if (!originID) { + // TODO, this should only happen when no data has loaded. + return taxiLayout; + } + + // find the origin node + const originNode = indexedProcessTreeModel.processEvent(indexedProcessTree, originID); + + if (!originNode) { + // this should only happen if the `ResolverTree` from the server has an entity ID with no matching lifecycle events. + throw new Error('Origin node not found in ResolverTree'); + } + + // Find the position of the origin, we'll center the map on it intrinsically + const originPosition = isometricTaxiLayoutModel.nodePosition(taxiLayout, originNode); + // adjust the position of everything so that the origin node is at `(0, 0)` + + if (originPosition === undefined) { + // not sure how this could happen. + return taxiLayout; + } + + // subtract the origin position from the layout, centering the layout around the origin node + return isometricTaxiLayoutModel.translated(taxiLayout, vector2.scale(originPosition, -1)); + } +); /** * Given a nodeID (aka entity_id) get the indexed process event. diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 09293d0b3b683..2a449efb295aa 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -251,6 +251,14 @@ export const ariaLevel: ( dataSelectors.ariaLevel ); +/** + * the node ID of the node representing the databaseDocumentID + */ +export const originID: (state: ResolverState) => string | undefined = composeSelectors( + dataStateSelector, + dataSelectors.originID +); + /** * Takes a nodeID (aka entity_id) and returns the node ID of the node that aria should 'flowto' or null * If the node has a flowto candidate that is currently visible, that will be returned, otherwise null. diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index 42f9634238e6a..fc4a9daf17ad1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -422,7 +422,7 @@ const processTypeToCube: Record = { export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleMap; - cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessOrigin: boolean) => NodeStyleConfig; + cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessTrigger: boolean) => NodeStyleConfig; } => { const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; @@ -497,10 +497,14 @@ export const useResolverTheme = (): { }, }; - function cubeAssetsForNode(isProcessTerminated: boolean, isProcessOrigin: boolean) { + function cubeAssetsForNode(isProcessTerminated: boolean, isProcessTrigger: boolean) { if (isProcessTerminated) { - return nodeAssets[processTypeToCube.processTerminated]; - } else if (isProcessOrigin) { + if (isProcessTrigger) { + return nodeAssets.terminatedTriggerCube; + } else { + return nodeAssets[processTypeToCube.processTerminated]; + } + } else if (isProcessTrigger) { return nodeAssets[processTypeToCube.processCausedAlert]; } else { return nodeAssets[processTypeToCube.processRan]; diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx index 930e96c3f3e40..ed483f0c736a2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx @@ -110,7 +110,6 @@ export const ResolverMap = React.memo(function ({ projectionMatrix={projectionMatrix} event={processEvent} isProcessTerminated={terminatedProcesses.has(processEntityId)} - isProcessOrigin={false} timeAtRender={timeAtRender} /> ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 1f4952f15119d..4e5f4a6979522 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -72,7 +72,6 @@ const UnstyledProcessEventDot = React.memo( event, projectionMatrix, isProcessTerminated, - isProcessOrigin, timeAtRender, }: { /** @@ -95,10 +94,6 @@ const UnstyledProcessEventDot = React.memo( * Whether or not to show the process as terminated. */ isProcessTerminated: boolean; - /** - * Whether or not to show the process as the originating event. - */ - isProcessOrigin: boolean; /** * The time (unix epoch) at render. @@ -212,7 +207,7 @@ const UnstyledProcessEventDot = React.memo( isLabelFilled, labelButtonFill, strokeColor, - } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin); + } = cubeAssetsForNode(isProcessTerminated, false); const labelHTMLID = htmlIdGenerator('resolver')(`${nodeID}:label`); From 468b04b517a81f98b6a01c3240c1c335cd9345f6 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 20 Jul 2020 14:53:52 -0400 Subject: [PATCH 2/8] fix selected node styling and aria-attributes UI state changes: * rename `activeDescendantId` to `ariaActiveDescendant`. it has the nodeID (aka entity_id) of the aria active descendant * rename `processEntityIdOfSelectedDescendant` to `selectedNode`. it has the nodeID (aka entity_id) of the selected node * aria html attributes were wrong (this also effected styling.) --- .../public/resolver/store/actions.ts | 23 +++------ .../public/resolver/store/reducer.ts | 50 +++++++------------ .../public/resolver/store/selectors.ts | 19 ++----- .../public/resolver/store/ui/selectors.ts | 23 +++------ .../public/resolver/types.ts | 12 ++--- .../public/resolver/view/map.tsx | 2 +- .../public/resolver/view/panel.tsx | 6 +-- .../resolver/view/process_event_dot.tsx | 21 +++----- 8 files changed, 51 insertions(+), 105 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index ac11ab8c88681..418eb0d837276 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -69,12 +69,9 @@ interface AppDetectedMissingEventData { */ interface UserFocusedOnResolverNode { readonly type: 'userFocusedOnResolverNode'; - readonly payload: { - /** - * Used to identify the process node that the user focused on (in the DOM) - */ - readonly nodeId: string; - }; + + /** focused nodeID */ + readonly payload: string; } /** @@ -85,16 +82,10 @@ interface UserFocusedOnResolverNode { */ interface UserSelectedResolverNode { readonly type: 'userSelectedResolverNode'; - readonly payload: { - /** - * The HTML ID used to identify the process node's element that the user selected - */ - readonly nodeId: string; - /** - * The process entity_id for the process the node represents - */ - readonly selectedProcessId: string; - }; + /** + * The nodeID (aka entity_id) that was select. + */ + readonly payload: string; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts index fc4c4de5819f3..028c28d94a41b 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { Reducer, combineReducers } from 'redux'; -import { htmlIdGenerator } from '@elastic/eui'; import { animateProcessIntoView } from './methods'; import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; @@ -12,51 +11,38 @@ import { ResolverAction } from './actions'; import { ResolverState, ResolverUIState } from '../types'; import { uniquePidForProcess } from '../models/process_event'; -/** - * Despite the name "generator", this function is entirely determinant - * (i.e. it will return the same html id given the same prefix 'resolverNode' - * and nodeId) - */ -const resolverNodeIdGenerator = htmlIdGenerator('resolverNode'); - const uiReducer: Reducer = ( - uiState = { - activeDescendantId: null, - selectedDescendantId: null, - processEntityIdOfSelectedDescendant: null, + state = { + ariaActiveDescendant: null, + selectedNode: null, }, action ) => { if (action.type === 'userFocusedOnResolverNode') { - return { - ...uiState, - activeDescendantId: action.payload.nodeId, + const next: ResolverUIState = { + ...state, + ariaActiveDescendant: action.payload, }; + return next; } else if (action.type === 'userSelectedResolverNode') { - return { - ...uiState, - selectedDescendantId: action.payload.nodeId, - processEntityIdOfSelectedDescendant: action.payload.selectedProcessId, + const next: ResolverUIState = { + ...state, + selectedNode: action.payload, }; + return next; } else if ( action.type === 'userBroughtProcessIntoView' || action.type === 'appDetectedNewIdFromQueryParams' ) { - /** - * This action has a process payload (instead of a processId), so we use - * `uniquePidForProcess` and `resolverNodeIdGenerator` to resolve the determinant - * html id of the node being brought into view. - */ - const processEntityId = uniquePidForProcess(action.payload.process); - const processNodeId = resolverNodeIdGenerator(processEntityId); - return { - ...uiState, - activeDescendantId: processNodeId, - selectedDescendantId: processNodeId, - processEntityIdOfSelectedDescendant: processEntityId, + const nodeID = uniquePidForProcess(action.payload.process); + const next: ResolverUIState = { + ...state, + ariaActiveDescendant: nodeID, + selectedNode: nodeID, }; + return next; } else { - return uiState; + return state; } }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 2a449efb295aa..66d7e04d118ed 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -144,26 +144,15 @@ export const relatedEventInfoByEntityId = composeSelectors( /** * Returns the id of the "current" tree node (fake-focused) */ -export const uiActiveDescendantId = composeSelectors( +export const ariaActiveDescendant = composeSelectors( uiStateSelector, - uiSelectors.activeDescendantId + uiSelectors.ariaActiveDescendant ); /** - * Returns the id of the "selected" tree node (the node that is currently "pressed" and possibly controlling other popups / components) + * Returns the nodeID of the selected node */ -export const uiSelectedDescendantId = composeSelectors( - uiStateSelector, - uiSelectors.selectedDescendantId -); - -/** - * Returns the entity_id of the "selected" tree node's process - */ -export const uiSelectedDescendantProcessId = composeSelectors( - uiStateSelector, - uiSelectors.selectedDescendantProcessId -); +export const selectedNode = composeSelectors(uiStateSelector, uiSelectors.selectedNode); /** * Returns the camera state from within ResolverState diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts index 494d8884329c6..91a2cbecbc04c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts @@ -10,32 +10,21 @@ import { ResolverUIState } from '../../types'; /** * id of the "current" tree node (fake-focused) */ -export const activeDescendantId = createSelector( +export const ariaActiveDescendant = createSelector( (uiState: ResolverUIState) => uiState, /* eslint-disable no-shadow */ - ({ activeDescendantId }) => { - return activeDescendantId; + ({ ariaActiveDescendant }) => { + return ariaActiveDescendant; } ); /** * id of the currently "selected" tree node */ -export const selectedDescendantId = createSelector( +export const selectedNode = createSelector( (uiState: ResolverUIState) => uiState, /* eslint-disable no-shadow */ - ({ selectedDescendantId }) => { - return selectedDescendantId; - } -); - -/** - * id of the currently "selected" tree node - */ -export const selectedDescendantProcessId = createSelector( - (uiState: ResolverUIState) => uiState, - /* eslint-disable no-shadow */ - ({ processEntityIdOfSelectedDescendant }: ResolverUIState) => { - return processEntityIdOfSelectedDescendant; + ({ selectedNode }: ResolverUIState) => { + return selectedNode; } ); diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 0272de0d8fd2a..856ae2d6240e3 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -34,17 +34,13 @@ export interface ResolverState { */ export interface ResolverUIState { /** - * The ID attribute of the resolver's aria-activedescendent. + * The nodeID for the process that is selected (in the aria-activedescendent sense of being selected.) */ - readonly activeDescendantId: string | null; + readonly ariaActiveDescendant: string | null; /** - * The ID attribute of the resolver's currently selected descendant. + * nodeID of the selected node */ - readonly selectedDescendantId: string | null; - /** - * The entity_id of the process for the resolver's currently selected descendant. - */ - readonly processEntityIdOfSelectedDescendant: string | null; + readonly selectedNode: string | null; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx index ed483f0c736a2..69ff9c8e2351b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx @@ -64,7 +64,7 @@ export const ResolverMap = React.memo(function ({ const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); const hasError = useSelector(selectors.hasError); - const activeDescendantId = useSelector(selectors.uiActiveDescendantId); + const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); return ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index efb2d95396ef5..cb0acdc29ceb1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -60,10 +60,10 @@ const PanelContent = memo(function PanelContent() { // The "selected" node (and its corresponding event) in the tree control. // It may need to be synchronized with the ID indicated as selected via the `idFromParams` // memo above. When this is the case, it is handled by the layout effect below. - const selectedDescendantProcessId = useSelector(selectors.uiSelectedDescendantProcessId); + const selectedNode = useSelector(selectors.selectedNode); const uiSelectedEvent = useMemo(() => { - return graphableProcesses.find((evt) => event.entityId(evt) === selectedDescendantProcessId); - }, [graphableProcesses, selectedDescendantProcessId]); + return graphableProcesses.find((evt) => event.entityId(evt) === selectedNode); + }, [graphableProcesses, selectedNode]); // Until an event is dispatched during update, the event indicated as selected by params may // be different than the one in state. diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 4e5f4a6979522..b83fa34dd76e5 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -112,8 +112,8 @@ const UnstyledProcessEventDot = React.memo( const [xScale] = projectionMatrix; // Node (html id=) IDs - const activeDescendantId = useSelector(selectors.uiActiveDescendantId); - const selectedDescendantId = useSelector(selectors.uiSelectedDescendantId); + const ariaActiveDescendant = useSelector(selectors.ariaActiveDescendant); + const selectedNode = useSelector(selectors.selectedNode); const nodeID = processEventModel.uniquePidForProcess(event); const relatedEventStats = useSelector(selectors.relatedEventsStats)(nodeID); @@ -211,19 +211,17 @@ const UnstyledProcessEventDot = React.memo( const labelHTMLID = htmlIdGenerator('resolver')(`${nodeID}:label`); - const isAriaCurrent = nodeID === activeDescendantId; - const isAriaSelected = nodeID === selectedDescendantId; + const isAriaCurrent = nodeID === ariaActiveDescendant; + const isAriaSelected = nodeID === selectedNode; const dispatch = useResolverDispatch(); const handleFocus = useCallback(() => { dispatch({ type: 'userFocusedOnResolverNode', - payload: { - nodeId: nodeHTMLID(nodeID), - }, + payload: nodeID, }); - }, [dispatch, nodeHTMLID, nodeID]); + }, [dispatch, nodeID]); const handleRelatedEventRequest = useCallback(() => { dispatch({ @@ -242,13 +240,10 @@ const UnstyledProcessEventDot = React.memo( } dispatch({ type: 'userSelectedResolverNode', - payload: { - nodeId: nodeHTMLID(nodeID), - selectedProcessId: nodeID, - }, + payload: nodeID, }); pushToQueryParams({ crumbId: nodeID, crumbEvent: '' }); - }, [animationTarget, dispatch, pushToQueryParams, nodeID, nodeHTMLID]); + }, [animationTarget, dispatch, pushToQueryParams, nodeID]); /** * Enumerates the stats for related events to display with the node as options, From 819176b887010c433e903bafdba63677c1a78b1d Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 20 Jul 2020 15:22:03 -0400 Subject: [PATCH 3/8] fix type error in generator code. this must not be in a ts project --- x-pack/plugins/security_solution/common/endpoint/index_data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 9a61738cd84b4..00b8f0b057afd 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -94,6 +94,6 @@ async function indexAlerts( }, [] ); - await client.bulk({ body, refresh: true }); + await client.bulk({ body, refresh: 'true' }); } } From 924926231d7f563a3e61cf6861c44a30f40361d7 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 20 Jul 2020 16:15:11 -0400 Subject: [PATCH 4/8] cleanup comments --- .../models/indexed_process_tree/isometric_taxi_layout.ts | 7 ++++++- .../public/resolver/store/data/selectors.ts | 5 +++-- .../public/resolver/view/process_event_dot.tsx | 7 ++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts index a9f3c46e375a8..1fc2ea0150aee 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts @@ -493,7 +493,12 @@ export function nodePosition(model: IsometricTaxiLayout, node: ResolverEvent): V } /** - * Return a clone of `model` with all positions incremented by `position`. + * Return a clone of `model` with all positions incremented by `translation`. + * Use this to move the layout around. + * e.g. + * ``` + * translated(layout, [100, -200]) // return a copy of `layout`, thats been moved 100 to the right and 200 up + * ``` */ export function translated(model: IsometricTaxiLayout, translation: Vector2): IsometricTaxiLayout { return { diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index bb736e9e98b5c..40138d3f2fd3c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -346,7 +346,7 @@ export const layout = createSelector( const taxiLayout = isometricTaxiLayoutModel.isometricTaxiLayoutFactory(indexedProcessTree); if (!originID) { - // TODO, this should only happen when no data has loaded. + // no data has loaded. return taxiLayout; } @@ -367,7 +367,8 @@ export const layout = createSelector( return taxiLayout; } - // subtract the origin position from the layout, centering the layout around the origin node + // Take the origin position, and multipy it by -1, then move the layout by that amount. + // This should center the layout around the origin. return isometricTaxiLayoutModel.translated(taxiLayout, vector2.scale(originPosition, -1)); } ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index b83fa34dd76e5..944f98a7d7f93 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -207,7 +207,12 @@ const UnstyledProcessEventDot = React.memo( isLabelFilled, labelButtonFill, strokeColor, - } = cubeAssetsForNode(isProcessTerminated, false); + } = cubeAssetsForNode( + isProcessTerminated, + /** + * There is no definition for 'trigger process' yet. return false. + */ false + ); const labelHTMLID = htmlIdGenerator('resolver')(`${nodeID}:label`); From c985ac891c42a15219cd8bd792d773776e033b76 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 20 Jul 2020 16:38:16 -0400 Subject: [PATCH 5/8] Reposition popovers when the map changes. --- .../public/resolver/view/process_event_dot.tsx | 1 + .../public/resolver/view/submenu.tsx | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 944f98a7d7f93..05f2e0cbfcfa9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -417,6 +417,7 @@ const UnstyledProcessEventDot = React.memo( buttonFill={colorMap.resolverBackground} menuAction={handleRelatedEventRequest} menuTitle={subMenuAssets.relatedEvents.title} + projectionMatrix={projectionMatrix} optionsWithActions={relatedEventStatusOrOptions} /> )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index ce126bf695559..5a05a46c0bf7b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -133,6 +133,7 @@ const NodeSubMenuComponents = React.memo( menuAction, optionsWithActions, className, + projectionMatrix, }: { menuTitle: string; className?: string; @@ -140,6 +141,10 @@ const NodeSubMenuComponents = React.memo( buttonBorderColor: ButtonColor; buttonFill: string; count?: number; + /** + * Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself. + */ + projectionMatrix: Matrix3; } & { optionsWithActions?: ResolverSubmenuOptionList | string | undefined; }) => { @@ -220,12 +225,21 @@ const NodeSubMenuComponents = React.memo( {menuIsOpen && typeof optionsWithActions === 'object' && ( )} + {/** + * EuiPopover has a mutation observer that observers the child tree and repositions itself. + * Abuse this to cause it to reposition itself when the project matrix changes. + */} + ); } ); +const HiddenSpan = styled('div')` + display: none; +`; + NodeSubMenuComponents.displayName = 'NodeSubMenu'; export const NodeSubMenu = styled(NodeSubMenuComponents)` From b77039443dd13666f315efe864f467920192b913 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 20 Jul 2020 17:05:23 -0400 Subject: [PATCH 6/8] reposition popovers in a different way --- .../public/resolver/view/submenu.tsx | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 5a05a46c0bf7b..409b3e5a2be79 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -5,7 +5,15 @@ */ import { i18n } from '@kbn/i18n'; -import React, { ReactNode, useState, useMemo, useCallback } from 'react'; +import React, { + ReactNode, + useState, + useMemo, + useCallback, + useRef, + useLayoutEffect, + useLayoutEffect, +} from 'react'; import { EuiI18nNumber, EuiSelectable, @@ -15,6 +23,7 @@ import { htmlIdGenerator, } from '@elastic/eui'; import styled from 'styled-components'; +import { Matrix3 } from '../types'; /** * i18n-translated titles for submenus and identifiers for display of states: @@ -148,6 +157,9 @@ const NodeSubMenuComponents = React.memo( } & { optionsWithActions?: ResolverSubmenuOptionList | string | undefined; }) => { + // keep a ref to the popover so we can call its reposition method + const popoverRef = useRef(null); + const [menuIsOpen, setMenuOpen] = useState(false); const handleMenuOpenClick = useCallback( (clickEvent: React.MouseEvent) => { @@ -174,6 +186,28 @@ const NodeSubMenuComponents = React.memo( const isMenuLoading = optionsWithActions === 'waitingForRelatedEventData'; + // The last projection matrix that was used to position the popover + const projectionMatrixAtLastRender = useRef(); + + useLayoutEffect(() => { + if ( + /** + * If there is a popover component reference, + * and this isn't the first render, + * and the projectionMatrix has changed since last render, + * then force the popover to reposition itself. + */ + popoverRef.current && + !projectionMatrixAtLastRender.current && + projectionMatrixAtLastRender.current !== projectionMatrix + ) { + popoverRef.current.positionPopoverFixed(); + } + + // no matter what, keep track of the last project matrix that was used to size the popover + projectionMatrixAtLastRender.current = projectionMatrix; + }, [projectionMatrixAtLastRender, projectionMatrix]); + if (!optionsWithActions) { /** * When called with a `menuAction` @@ -221,25 +255,17 @@ const NodeSubMenuComponents = React.memo( isOpen={menuIsOpen} closePopover={closePopover} repositionOnScroll + ref={popoverRef} > {menuIsOpen && typeof optionsWithActions === 'object' && ( )} - {/** - * EuiPopover has a mutation observer that observers the child tree and repositions itself. - * Abuse this to cause it to reposition itself when the project matrix changes. - */} - ); } ); -const HiddenSpan = styled('div')` - display: none; -`; - NodeSubMenuComponents.displayName = 'NodeSubMenu'; export const NodeSubMenu = styled(NodeSubMenuComponents)` From b8a0e68b8e97841036664010a62b99f1564d27dc Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 21 Jul 2020 10:04:59 -0400 Subject: [PATCH 7/8] fix extra import --- .../security_solution/public/resolver/view/submenu.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 409b3e5a2be79..2499a451b9c8c 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -5,15 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import React, { - ReactNode, - useState, - useMemo, - useCallback, - useRef, - useLayoutEffect, - useLayoutEffect, -} from 'react'; +import React, { ReactNode, useState, useMemo, useCallback, useRef, useLayoutEffect } from 'react'; import { EuiI18nNumber, EuiSelectable, From d8e051c2596d6d7c7197cdb5e7a103dee7f21717 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 21 Jul 2020 11:30:27 -0400 Subject: [PATCH 8/8] undo ts fix. it doesnt work --- x-pack/plugins/security_solution/common/endpoint/index_data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 00b8f0b057afd..9a61738cd84b4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -94,6 +94,6 @@ async function indexAlerts( }, [] ); - await client.bulk({ body, refresh: 'true' }); + await client.bulk({ body, refresh: true }); } }