Skip to content

Commit b930cef

Browse files
author
Robert Austin
authored
[Resolver] Origin process (#72382)
Co-authored-by: Brent Kimmel <brent.kimmel@elastic.co> * Center the origin node * Nodes appear selected when they are selected. also the aria attributes are working. * Reposition the submenu when the user pans.
1 parent bb7d128 commit b930cef

File tree

13 files changed

+214
-139
lines changed

13 files changed

+214
-139
lines changed

x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
import { IsometricTaxiLayout } from '../../types';
77
import { LegacyEndpointEvent } from '../../../../common/endpoint/types';
8-
import { isometricTaxiLayout } from './isometric_taxi_layout';
8+
import { isometricTaxiLayoutFactory } from './isometric_taxi_layout';
99
import { mockProcessEvent } from '../../models/process_event_test_helpers';
1010
import { factory } from './index';
1111

@@ -107,7 +107,7 @@ describe('resolver graph layout', () => {
107107
unique_ppid: 0,
108108
},
109109
});
110-
layout = () => isometricTaxiLayout(factory(events));
110+
layout = () => isometricTaxiLayoutFactory(factory(events));
111111
events = [];
112112
});
113113
describe('when rendering no nodes', () => {

x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6-
import * as vector2 from '../../models/vector2';
76
import {
87
IndexedProcessTree,
98
Vector2,
@@ -17,14 +16,17 @@ import {
1716
} from '../../types';
1817
import * as event from '../../../../common/endpoint/models/event';
1918
import { ResolverEvent } from '../../../../common/endpoint/types';
20-
import * as model from './index';
19+
import * as vector2 from '../vector2';
20+
import * as indexedProcessTreeModel from './index';
2121
import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date';
2222
import { uniquePidForProcess } from '../process_event';
2323

2424
/**
2525
* Graph the process tree
2626
*/
27-
export function isometricTaxiLayout(indexedProcessTree: IndexedProcessTree): IsometricTaxiLayout {
27+
export function isometricTaxiLayoutFactory(
28+
indexedProcessTree: IndexedProcessTree
29+
): IsometricTaxiLayout {
2830
/**
2931
* Walk the tree in reverse level order, calculating the 'width' of subtrees.
3032
*/
@@ -83,8 +85,8 @@ export function isometricTaxiLayout(indexedProcessTree: IndexedProcessTree): Iso
8385
*/
8486
function ariaLevels(indexedProcessTree: IndexedProcessTree): Map<ResolverEvent, number> {
8587
const map: Map<ResolverEvent, number> = new Map();
86-
for (const node of model.levelOrder(indexedProcessTree)) {
87-
const parentNode = model.parent(indexedProcessTree, node);
88+
for (const node of indexedProcessTreeModel.levelOrder(indexedProcessTree)) {
89+
const parentNode = indexedProcessTreeModel.parent(indexedProcessTree, node);
8890
if (parentNode === undefined) {
8991
// nodes at the root have a level of 1
9092
map.set(node, 1);
@@ -143,16 +145,19 @@ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map<ResolverEvent,
143145
function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths {
144146
const widths = new Map<ResolverEvent, number>();
145147

146-
if (model.size(indexedProcessTree) === 0) {
148+
if (indexedProcessTreeModel.size(indexedProcessTree) === 0) {
147149
return widths;
148150
}
149151

150152
const processesInReverseLevelOrder: ResolverEvent[] = [
151-
...model.levelOrder(indexedProcessTree),
153+
...indexedProcessTreeModel.levelOrder(indexedProcessTree),
152154
].reverse();
153155

154156
for (const process of processesInReverseLevelOrder) {
155-
const children = model.children(indexedProcessTree, uniquePidForProcess(process));
157+
const children = indexedProcessTreeModel.children(
158+
indexedProcessTree,
159+
uniquePidForProcess(process)
160+
);
156161

157162
const sumOfWidthOfChildren = function sumOfWidthOfChildren() {
158163
return children.reduce(function sum(currentValue, child) {
@@ -229,7 +234,10 @@ function processEdgeLineSegments(
229234
metadata: edgeLineMetadata,
230235
};
231236

232-
const siblings = model.children(indexedProcessTree, uniquePidForProcess(parent));
237+
const siblings = indexedProcessTreeModel.children(
238+
indexedProcessTree,
239+
uniquePidForProcess(parent)
240+
);
233241
const isFirstChild = process === siblings[0];
234242

235243
if (metadata.isOnlyChild) {
@@ -384,8 +392,8 @@ function* levelOrderWithWidths(
384392
tree: IndexedProcessTree,
385393
widths: ProcessWidths
386394
): Iterable<ProcessWithWidthMetadata> {
387-
for (const process of model.levelOrder(tree)) {
388-
const parent = model.parent(tree, process);
395+
for (const process of indexedProcessTreeModel.levelOrder(tree)) {
396+
const parent = indexedProcessTreeModel.parent(tree, process);
389397
const width = widths.get(process);
390398

391399
if (width === undefined) {
@@ -423,7 +431,7 @@ function* levelOrderWithWidths(
423431
parentWidth,
424432
};
425433

426-
const siblings = model.children(tree, uniquePidForProcess(parent));
434+
const siblings = indexedProcessTreeModel.children(tree, uniquePidForProcess(parent));
427435
if (siblings.length === 1) {
428436
metadata.isOnlyChild = true;
429437
metadata.lastChildWidth = width;
@@ -479,3 +487,32 @@ const distanceBetweenNodesInUnits = 2;
479487
* The distance in pixels (at scale 1) between nodes. Change this to space out nodes more
480488
*/
481489
const distanceBetweenNodes = distanceBetweenNodesInUnits * unit;
490+
491+
export function nodePosition(model: IsometricTaxiLayout, node: ResolverEvent): Vector2 | undefined {
492+
return model.processNodePositions.get(node);
493+
}
494+
495+
/**
496+
* Return a clone of `model` with all positions incremented by `translation`.
497+
* Use this to move the layout around.
498+
* e.g.
499+
* ```
500+
* translated(layout, [100, -200]) // return a copy of `layout`, thats been moved 100 to the right and 200 up
501+
* ```
502+
*/
503+
export function translated(model: IsometricTaxiLayout, translation: Vector2): IsometricTaxiLayout {
504+
return {
505+
processNodePositions: new Map(
506+
[...model.processNodePositions.entries()].map(([node, position]) => [
507+
node,
508+
vector2.add(position, translation),
509+
])
510+
),
511+
edgeLineSegments: model.edgeLineSegments.map(({ points, metadata }) => ({
512+
points: points.map((point) => vector2.add(point, translation)),
513+
metadata,
514+
})),
515+
// these are unchanged
516+
ariaLevels: model.ariaLevels,
517+
};
518+
}

x-pack/plugins/security_solution/public/resolver/store/actions.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,9 @@ interface AppDetectedMissingEventData {
6969
*/
7070
interface UserFocusedOnResolverNode {
7171
readonly type: 'userFocusedOnResolverNode';
72-
readonly payload: {
73-
/**
74-
* Used to identify the process node that the user focused on (in the DOM)
75-
*/
76-
readonly nodeId: string;
77-
};
72+
73+
/** focused nodeID */
74+
readonly payload: string;
7875
}
7976

8077
/**
@@ -85,16 +82,10 @@ interface UserFocusedOnResolverNode {
8582
*/
8683
interface UserSelectedResolverNode {
8784
readonly type: 'userSelectedResolverNode';
88-
readonly payload: {
89-
/**
90-
* The HTML ID used to identify the process node's element that the user selected
91-
*/
92-
readonly nodeId: string;
93-
/**
94-
* The process entity_id for the process the node represents
95-
*/
96-
readonly selectedProcessId: string;
97-
};
85+
/**
86+
* The nodeID (aka entity_id) that was select.
87+
*/
88+
readonly payload: string;
9889
}
9990

10091
/**

x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ import {
3030
ResolverRelatedEvents,
3131
} from '../../../../common/endpoint/types';
3232
import * as resolverTreeModel from '../../models/resolver_tree';
33-
import { isometricTaxiLayout } from '../../models/indexed_process_tree/isometric_taxi_layout';
33+
import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout';
3434
import { allEventCategories } from '../../../../common/endpoint/models/event';
35+
import * as vector2 from '../../models/vector2';
3536

3637
/**
3738
* If there is currently a request.
@@ -70,6 +71,21 @@ const resolverTreeResponse = (state: DataState): ResolverTree | undefined => {
7071
}
7172
};
7273

74+
/**
75+
* the node ID of the node representing the databaseDocumentID.
76+
* NB: this could be stale if the last response is stale
77+
*/
78+
export const originID: (state: DataState) => string | undefined = createSelector(
79+
resolverTreeResponse,
80+
function (resolverTree?) {
81+
if (resolverTree) {
82+
// This holds the entityID (aka nodeID) of the node related to the last fetched `_id`
83+
return resolverTree.entityID;
84+
}
85+
return undefined;
86+
}
87+
);
88+
7389
/**
7490
* Process events that will be displayed as terminated.
7591
*/
@@ -317,13 +333,45 @@ export function databaseDocumentIDToFetch(state: DataState): string | null {
317333
}
318334
}
319335

320-
export const layout = createSelector(tree, function processNodePositionsAndEdgeLineSegments(
321-
/* eslint-disable no-shadow */
322-
indexedProcessTree
323-
/* eslint-enable no-shadow */
324-
) {
325-
return isometricTaxiLayout(indexedProcessTree);
326-
});
336+
export const layout = createSelector(
337+
tree,
338+
originID,
339+
function processNodePositionsAndEdgeLineSegments(
340+
/* eslint-disable no-shadow */
341+
indexedProcessTree,
342+
originID
343+
/* eslint-enable no-shadow */
344+
) {
345+
// use the isometric taxi layout as a base
346+
const taxiLayout = isometricTaxiLayoutModel.isometricTaxiLayoutFactory(indexedProcessTree);
347+
348+
if (!originID) {
349+
// no data has loaded.
350+
return taxiLayout;
351+
}
352+
353+
// find the origin node
354+
const originNode = indexedProcessTreeModel.processEvent(indexedProcessTree, originID);
355+
356+
if (!originNode) {
357+
// this should only happen if the `ResolverTree` from the server has an entity ID with no matching lifecycle events.
358+
throw new Error('Origin node not found in ResolverTree');
359+
}
360+
361+
// Find the position of the origin, we'll center the map on it intrinsically
362+
const originPosition = isometricTaxiLayoutModel.nodePosition(taxiLayout, originNode);
363+
// adjust the position of everything so that the origin node is at `(0, 0)`
364+
365+
if (originPosition === undefined) {
366+
// not sure how this could happen.
367+
return taxiLayout;
368+
}
369+
370+
// Take the origin position, and multipy it by -1, then move the layout by that amount.
371+
// This should center the layout around the origin.
372+
return isometricTaxiLayoutModel.translated(taxiLayout, vector2.scale(originPosition, -1));
373+
}
374+
);
327375

328376
/**
329377
* Given a nodeID (aka entity_id) get the indexed process event.

x-pack/plugins/security_solution/public/resolver/store/reducer.ts

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,45 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66
import { Reducer, combineReducers } from 'redux';
7-
import { htmlIdGenerator } from '@elastic/eui';
87
import { animateProcessIntoView } from './methods';
98
import { cameraReducer } from './camera/reducer';
109
import { dataReducer } from './data/reducer';
1110
import { ResolverAction } from './actions';
1211
import { ResolverState, ResolverUIState } from '../types';
1312
import { uniquePidForProcess } from '../models/process_event';
1413

15-
/**
16-
* Despite the name "generator", this function is entirely determinant
17-
* (i.e. it will return the same html id given the same prefix 'resolverNode'
18-
* and nodeId)
19-
*/
20-
const resolverNodeIdGenerator = htmlIdGenerator('resolverNode');
21-
2214
const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
23-
uiState = {
24-
activeDescendantId: null,
25-
selectedDescendantId: null,
26-
processEntityIdOfSelectedDescendant: null,
15+
state = {
16+
ariaActiveDescendant: null,
17+
selectedNode: null,
2718
},
2819
action
2920
) => {
3021
if (action.type === 'userFocusedOnResolverNode') {
31-
return {
32-
...uiState,
33-
activeDescendantId: action.payload.nodeId,
22+
const next: ResolverUIState = {
23+
...state,
24+
ariaActiveDescendant: action.payload,
3425
};
26+
return next;
3527
} else if (action.type === 'userSelectedResolverNode') {
36-
return {
37-
...uiState,
38-
selectedDescendantId: action.payload.nodeId,
39-
processEntityIdOfSelectedDescendant: action.payload.selectedProcessId,
28+
const next: ResolverUIState = {
29+
...state,
30+
selectedNode: action.payload,
4031
};
32+
return next;
4133
} else if (
4234
action.type === 'userBroughtProcessIntoView' ||
4335
action.type === 'appDetectedNewIdFromQueryParams'
4436
) {
45-
/**
46-
* This action has a process payload (instead of a processId), so we use
47-
* `uniquePidForProcess` and `resolverNodeIdGenerator` to resolve the determinant
48-
* html id of the node being brought into view.
49-
*/
50-
const processEntityId = uniquePidForProcess(action.payload.process);
51-
const processNodeId = resolverNodeIdGenerator(processEntityId);
52-
return {
53-
...uiState,
54-
activeDescendantId: processNodeId,
55-
selectedDescendantId: processNodeId,
56-
processEntityIdOfSelectedDescendant: processEntityId,
37+
const nodeID = uniquePidForProcess(action.payload.process);
38+
const next: ResolverUIState = {
39+
...state,
40+
ariaActiveDescendant: nodeID,
41+
selectedNode: nodeID,
5742
};
43+
return next;
5844
} else {
59-
return uiState;
45+
return state;
6046
}
6147
};
6248

x-pack/plugins/security_solution/public/resolver/store/selectors.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -144,26 +144,15 @@ export const relatedEventInfoByEntityId = composeSelectors(
144144
/**
145145
* Returns the id of the "current" tree node (fake-focused)
146146
*/
147-
export const uiActiveDescendantId = composeSelectors(
147+
export const ariaActiveDescendant = composeSelectors(
148148
uiStateSelector,
149-
uiSelectors.activeDescendantId
149+
uiSelectors.ariaActiveDescendant
150150
);
151151

152152
/**
153-
* Returns the id of the "selected" tree node (the node that is currently "pressed" and possibly controlling other popups / components)
153+
* Returns the nodeID of the selected node
154154
*/
155-
export const uiSelectedDescendantId = composeSelectors(
156-
uiStateSelector,
157-
uiSelectors.selectedDescendantId
158-
);
159-
160-
/**
161-
* Returns the entity_id of the "selected" tree node's process
162-
*/
163-
export const uiSelectedDescendantProcessId = composeSelectors(
164-
uiStateSelector,
165-
uiSelectors.selectedDescendantProcessId
166-
);
155+
export const selectedNode = composeSelectors(uiStateSelector, uiSelectors.selectedNode);
167156

168157
/**
169158
* Returns the camera state from within ResolverState
@@ -251,6 +240,14 @@ export const ariaLevel: (
251240
dataSelectors.ariaLevel
252241
);
253242

243+
/**
244+
* the node ID of the node representing the databaseDocumentID
245+
*/
246+
export const originID: (state: ResolverState) => string | undefined = composeSelectors(
247+
dataStateSelector,
248+
dataSelectors.originID
249+
);
250+
254251
/**
255252
* Takes a nodeID (aka entity_id) and returns the node ID of the node that aria should 'flowto' or null
256253
* If the node has a flowto candidate that is currently visible, that will be returned, otherwise null.

0 commit comments

Comments
 (0)