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 4343793648993..dc17fc70ef8af 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 @@ -329,7 +329,7 @@ export const layout = createSelector( * Legacy functions take process events instead of nodeID, use this to get * process events for them. */ -const processEventForID: ( +export const processEventForID: ( state: DataState ) => (nodeID: string) => ResolverEvent | null = createSelector( indexedProcessTree, diff --git a/x-pack/plugins/security_solution/public/resolver/store/methods.ts b/x-pack/plugins/security_solution/public/resolver/store/methods.ts index 3890770259156..ad06ddf36161a 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/methods.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/methods.ts @@ -5,7 +5,7 @@ */ import { animatePanning } from './camera/methods'; -import { processNodePositionsAndEdgeLineSegments } from './selectors'; +import { layout } from './selectors'; import { ResolverState } from '../types'; import { ResolverEvent } from '../../../common/endpoint/types'; @@ -19,7 +19,7 @@ export function animateProcessIntoView( startTime: number, process: ResolverEvent ): ResolverState { - const { processNodePositions } = processNodePositionsAndEdgeLineSegments(state); + const { processNodePositions } = layout(state); const position = processNodePositions.get(process); if (position) { return { diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts index 71118fc71a827..ba4a5a169c549 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts @@ -25,31 +25,31 @@ describe('resolver selectors', () => { return store.getState(); }; describe('ariaFlowtoNodeID', () => { - describe('when all nodes are in view', () => { + describe('with a tree with no descendants and 2 ancestors', () => { + const originID = 'c'; + const firstAncestorID = 'b'; + const secondAncestorID = 'a'; beforeEach(() => { - const size = 1000000; actions.push({ - // set the size of the camera - type: 'userSetRasterSize', - payload: [size, size], + type: 'serverReturnedResolverData', + payload: { + result: treeWith2AncestorsAndNoChildren({ + originID, + firstAncestorID, + secondAncestorID, + }), + // this value doesn't matter + databaseDocumentID: '', + }, }); }); - describe('with a tree with no descendants and 2 ancestors', () => { - const originID = 'c'; - const firstAncestorID = 'b'; - const secondAncestorID = 'a'; + describe('when all nodes are in view', () => { beforeEach(() => { + const size = 1000000; actions.push({ - type: 'serverReturnedResolverData', - payload: { - result: treeWith2AncestorsAndNoChildren({ - originID, - firstAncestorID, - secondAncestorID, - }), - // this value doesn't matter - databaseDocumentID: '', - }, + // set the size of the camera + type: 'userSetRasterSize', + payload: [size, size], }); }); it('should return no flowto for the second ancestor', () => { @@ -62,18 +62,28 @@ describe('resolver selectors', () => { expect(selectors.ariaFlowtoNodeID(state())(0)(originID)).toBe(null); }); }); - describe('with a tree with 2 children and no ancestors', () => { - const originID = 'c'; - const firstChildID = 'd'; - const secondChildID = 'e'; + }); + describe('with a tree with 2 children and no ancestors', () => { + const originID = 'c'; + const firstChildID = 'd'; + const secondChildID = 'e'; + beforeEach(() => { + actions.push({ + type: 'serverReturnedResolverData', + payload: { + result: treeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), + // this value doesn't matter + databaseDocumentID: '', + }, + }); + }); + describe('when all nodes are in view', () => { beforeEach(() => { + const rasterSize = 1000000; actions.push({ - type: 'serverReturnedResolverData', - payload: { - result: treeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), - // this value doesn't matter - databaseDocumentID: '', - }, + // set the size of the camera + type: 'userSetRasterSize', + payload: [rasterSize, rasterSize], }); }); it('should return no flowto for the origin', () => { @@ -86,6 +96,56 @@ describe('resolver selectors', () => { expect(selectors.ariaFlowtoNodeID(state())(0)(secondChildID)).toBe(null); }); }); + describe('when only the origin and first child are in view', () => { + beforeEach(() => { + // set the raster size + const rasterSize = 1000000; + actions.push({ + // set the size of the camera + type: 'userSetRasterSize', + payload: [rasterSize, rasterSize], + }); + + // get the layout + const layout = selectors.layout(state()); + + // find the position of the second child + const secondChild = selectors.processEventForID(state())(secondChildID); + const positionOfSecondChild = layout.processNodePositions.get(secondChild!)!; + + // the child is indexed by an AABB that extends -720/2 to the left + const leftSideOfSecondChildAABB = positionOfSecondChild[0] - 720 / 2; + + // adjust the camera so that it doesn't quite see the second child + actions.push({ + // set the position of the camera so that the left edge of the second child is at the right edge + // of the viewable area + type: 'userSetPositionOfCamera', + payload: [rasterSize / -2 + leftSideOfSecondChildAABB, 0], + }); + }); + it('the origin should be in view', () => { + const origin = selectors.processEventForID(state())(originID)!; + expect( + selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(origin) + ).toBe(true); + }); + it('the first child should be in view', () => { + const firstChild = selectors.processEventForID(state())(firstChildID)!; + expect( + selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(firstChild) + ).toBe(true); + }); + it('the second child should not be in view', () => { + const secondChild = selectors.processEventForID(state())(secondChildID)!; + expect( + selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(secondChild) + ).toBe(false); + }); + it('should return nothing as the flowto for the first child', () => { + expect(selectors.ariaFlowtoNodeID(state())(0)(firstChildID)).toBe(null); + }); + }); }); }); }); 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 c6621a2b015c6..ff2179dc3a2ae 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -8,8 +8,9 @@ import { createSelector, defaultMemoize } from 'reselect'; import * as cameraSelectors from './camera/selectors'; import * as dataSelectors from './data/selectors'; import * as uiSelectors from './ui/selectors'; -import { ResolverState } from '../types'; +import { ResolverState, IsometricTaxiLayout } from '../types'; import { uniquePidForProcess } from '../models/process_event'; +import { ResolverEvent } from '../../../common/endpoint/types'; /** * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. @@ -52,7 +53,22 @@ export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelecto */ export const isAnimating = composeSelectors(cameraStateSelector, cameraSelectors.isAnimating); -export const processNodePositionsAndEdgeLineSegments = composeSelectors( +/** + * Given a nodeID (aka entity_id) get the indexed process event. + * Legacy functions take process events instead of nodeID, use this to get + * process events for them. + */ +export const processEventForID: ( + state: ResolverState +) => (nodeID: string) => ResolverEvent | null = composeSelectors( + dataStateSelector, + dataSelectors.processEventForID +); + +/** + * The position of nodes and edges. + */ +export const layout: (state: ResolverState) => IsometricTaxiLayout = composeSelectors( dataStateSelector, dataSelectors.layout ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx index 0ed677885775f..6f9bfad8c08c2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx @@ -146,7 +146,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ [pushToQueryParams, handleBringIntoViewClick, isProcessOrigin, isProcessTerminated] ); - const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); + const { processNodePositions } = useSelector(selectors.layout); const processTableView: ProcessTableView[] = useMemo( () => [...processNodePositions.keys()].map((processEvent) => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 3476764a88733..a27f157bc9364 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -189,9 +189,7 @@ describe('useCamera on an unpainted element', () => { throw new Error('failed to create tree'); } const processes: ResolverEvent[] = [ - ...selectors - .processNodePositionsAndEdgeLineSegments(store.getState()) - .processNodePositions.keys(), + ...selectors.layout(store.getState()).processNodePositions.keys(), ]; process = processes[processes.length - 1]; if (!process) {