[Endpoint] use rbush to only render to DOM resolver nodes that are in view (#68957)

* [Endpoint] use rbush to only render resolver nodes that are in view in the DOM

* Add related events code back

* Change processNodePositionsAndEdgeLineSegments selector to return a function that takes optional bounding box

* Refactor selectors to not break original, and not run as often

* Memoize rtree search selector, fix tests

* Update node styles to use style hook, update jest tests

* Fix type change issue in jest test
This commit is contained in:
Kevin Qualters 2020-06-26 09:42:10 -04:00 committed by GitHub
parent f1a1178328
commit 9ebf41c77c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 527 additions and 72 deletions

View file

@ -16,9 +16,11 @@
"@types/lodash": "^4.14.110" "@types/lodash": "^4.14.110"
}, },
"dependencies": { "dependencies": {
"@types/rbush": "^3.0.0",
"@types/seedrandom": ">=2.0.0 <4.0.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"querystring": "^0.2.0", "querystring": "^0.2.0",
"redux-devtools-extension": "^2.13.8", "rbush": "^3.0.1",
"@types/seedrandom": ">=2.0.0 <4.0.0" "redux-devtools-extension": "^2.13.8"
} }
} }

View file

@ -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 * as vector2 from './vector2';
import { AABB } from '../types';
/**
* Return a boolean indicating if 2 vector objects are equal.
*/
export function isEqual(a: AABB, b: AABB): boolean {
return vector2.isEqual(a.minimum, b.minimum) && vector2.isEqual(a.maximum, b.maximum);
}

View file

@ -40,6 +40,13 @@ export function applyMatrix3([x, y]: Vector2, [m11, m12, m13, m21, m22, m23]: Ma
return [x * m11 + y * m12 + m13, x * m21 + y * m22 + m23]; return [x * m11 + y * m12 + m13, x * m21 + y * m22 + m23];
} }
/**
* Returns a boolean indicating equality of two vectors.
*/
export function isEqual([x1, y1]: Vector2, [x2, y2]: Vector2): boolean {
return x1 === x2 && y1 === y2;
}
/** /**
* Returns the distance between two vectors * Returns the distance between two vectors
*/ */

View file

@ -24,6 +24,10 @@ function isValue(field: string | string[], value: string) {
} }
} }
export function isTerminatedProcess(passedEvent: ResolverEvent) {
return eventType(passedEvent) === 'processTerminated';
}
/** /**
* Returns a custom event type for a process event based on the event's metadata. * Returns a custom event type for a process event based on the event's metadata.
*/ */

View file

@ -36,6 +36,9 @@ exports[`resolver graph layout when rendering two forks, and one fork has an ext
Object { Object {
"edgeLineSegments": Array [ "edgeLineSegments": Array [
Object { Object {
"metadata": Object {
"uniqueId": "parentToMid",
},
"points": Array [ "points": Array [
Array [ Array [
0, 0,
@ -48,6 +51,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {
"uniqueId": "midway",
},
"points": Array [ "points": Array [
Array [ Array [
0, 0,
@ -60,7 +66,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {}, "metadata": Object {
"uniqueId": "",
},
"points": Array [ "points": Array [
Array [ Array [
0, 0,
@ -73,7 +81,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {}, "metadata": Object {
"uniqueId": "",
},
"points": Array [ "points": Array [
Array [ Array [
395.9797974644666, 395.9797974644666,
@ -86,6 +96,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {
"uniqueId": "parentToMid13",
},
"points": Array [ "points": Array [
Array [ Array [
197.9898987322333, 197.9898987322333,
@ -98,6 +111,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {
"uniqueId": "midway13",
},
"points": Array [ "points": Array [
Array [ Array [
296.98484809834997, 296.98484809834997,
@ -110,7 +126,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {}, "metadata": Object {
"uniqueId": "13",
},
"points": Array [ "points": Array [
Array [ Array [
296.98484809834997, 296.98484809834997,
@ -123,7 +141,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {}, "metadata": Object {
"uniqueId": "14",
},
"points": Array [ "points": Array [
Array [ Array [
494.9747468305833, 494.9747468305833,
@ -136,6 +156,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {
"uniqueId": "parentToMid25",
},
"points": Array [ "points": Array [
Array [ Array [
593.9696961966999, 593.9696961966999,
@ -148,6 +171,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {
"uniqueId": "midway25",
},
"points": Array [ "points": Array [
Array [ Array [
692.9646455628166, 692.9646455628166,
@ -160,7 +186,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {}, "metadata": Object {
"uniqueId": "25",
},
"points": Array [ "points": Array [
Array [ Array [
692.9646455628166, 692.9646455628166,
@ -173,7 +201,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {}, "metadata": Object {
"uniqueId": "26",
},
"points": Array [ "points": Array [
Array [ Array [
890.9545442950499, 890.9545442950499,
@ -186,7 +216,9 @@ Object {
], ],
}, },
Object { Object {
"metadata": Object {}, "metadata": Object {
"uniqueId": "67",
},
"points": Array [ "points": Array [
Array [ Array [
1088.9444430272833, 1088.9444430272833,
@ -344,7 +376,9 @@ exports[`resolver graph layout when rendering two nodes, one being the parent of
Object { Object {
"edgeLineSegments": Array [ "edgeLineSegments": Array [
Object { Object {
"metadata": Object {}, "metadata": Object {
"uniqueId": "",
},
"points": Array [ "points": Array [
Array [ Array [
0, 0,

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import rbush from 'rbush';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { import {
DataState, DataState,
@ -16,11 +17,20 @@ import {
AdjacentProcessMap, AdjacentProcessMap,
Vector2, Vector2,
EdgeLineMetadata, EdgeLineMetadata,
IndexedEntity,
IndexedEdgeLineSegment,
IndexedProcessNode,
AABB,
VisibleEntites,
} from '../../types'; } from '../../types';
import { ResolverEvent } from '../../../../common/endpoint/types'; import { ResolverEvent } from '../../../../common/endpoint/types';
import { eventTimestamp } from '../../../../common/endpoint/models/event'; import * as event from '../../../../common/endpoint/models/event';
import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2';
import { isGraphableProcess, uniquePidForProcess } from '../../models/process_event'; import {
isGraphableProcess,
isTerminatedProcess,
uniquePidForProcess,
} from '../../models/process_event';
import { import {
factory as indexedProcessTreeFactory, factory as indexedProcessTreeFactory,
children as indexedProcessTreeChildren, children as indexedProcessTreeChildren,
@ -29,6 +39,7 @@ import {
levelOrder, levelOrder,
} from '../../models/indexed_process_tree'; } from '../../models/indexed_process_tree';
import { getFriendlyElapsedTime } from '../../lib/date'; import { getFriendlyElapsedTime } from '../../lib/date';
import { isEqual } from '../../lib/aabb';
const unit = 140; const unit = 140;
const distanceBetweenNodesInUnits = 2; const distanceBetweenNodesInUnits = 2;
@ -81,6 +92,20 @@ export const graphableProcesses = createSelector(
} }
); );
/**
* Process events that will be displayed as terminated.
*/
export const terminatedProcesses = createSelector(
({ results }: DataState) => results,
function (results: DataState['results']) {
return new Set(
results.filter(isTerminatedProcess).map((terminatedEvent) => {
return uniquePidForProcess(terminatedEvent);
})
);
}
);
/** /**
* In laying out the graph, we precalculate the 'width' of each subtree. The 'width' of the subtree is determined by its * 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 * descedants and the rule that each process node must be at least 1 unit apart. Enforcing that all nodes are at least
@ -157,7 +182,7 @@ function processEdgeLineSegments(
): EdgeLineSegment[] { ): EdgeLineSegment[] {
const edgeLineSegments: EdgeLineSegment[] = []; const edgeLineSegments: EdgeLineSegment[] = [];
for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) {
const edgeLineMetadata: EdgeLineMetadata = {}; const edgeLineMetadata: EdgeLineMetadata = { uniqueId: '' };
/** /**
* We only handle children, drawing lines back to their parents. The root has no parent, so we skip it * We only handle children, drawing lines back to their parents. The root has no parent, so we skip it
*/ */
@ -168,6 +193,9 @@ function processEdgeLineSegments(
const { process, parent, parentWidth } = metadata; const { process, parent, parentWidth } = metadata;
const position = positions.get(process); const position = positions.get(process);
const parentPosition = positions.get(parent); const parentPosition = positions.get(parent);
const parentId = event.entityId(parent);
const processEntityId = event.entityId(process);
const edgeLineId = parentId ? parentId + processEntityId : parentId;
if (position === undefined || parentPosition === undefined) { if (position === undefined || parentPosition === undefined) {
/** /**
@ -176,12 +204,13 @@ function processEdgeLineSegments(
throw new Error(); throw new Error();
} }
const parentTime = eventTimestamp(parent); const parentTime = event.eventTimestamp(parent);
const processTime = eventTimestamp(process); const processTime = event.eventTimestamp(process);
if (parentTime && processTime) { if (parentTime && processTime) {
const elapsedTime = getFriendlyElapsedTime(parentTime, processTime); const elapsedTime = getFriendlyElapsedTime(parentTime, processTime);
if (elapsedTime) edgeLineMetadata.elapsedTime = elapsedTime; if (elapsedTime) edgeLineMetadata.elapsedTime = elapsedTime;
} }
edgeLineMetadata.uniqueId = edgeLineId;
/** /**
* The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line * The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line
@ -226,6 +255,7 @@ function processEdgeLineSegments(
const lineFromParentToMidwayLine: EdgeLineSegment = { const lineFromParentToMidwayLine: EdgeLineSegment = {
points: [parentPosition, [parentPosition[0], midwayY]], points: [parentPosition, [parentPosition[0], midwayY]],
metadata: { uniqueId: `parentToMid${edgeLineId}` },
}; };
const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2;
@ -246,6 +276,7 @@ function processEdgeLineSegments(
midwayY, midwayY,
], ],
], ],
metadata: { uniqueId: `midway${edgeLineId}` },
}; };
edgeLineSegments.push( edgeLineSegments.push(
@ -508,18 +539,16 @@ export const processNodePositionsAndEdgeLineSegments = createSelector(
for (const edgeLineSegment of edgeLineSegments) { for (const edgeLineSegment of edgeLineSegments) {
const { const {
points: [startPoint, endPoint], points: [startPoint, endPoint],
metadata,
} = edgeLineSegment; } = edgeLineSegment;
const transformedSegment: EdgeLineSegment = { const transformedSegment: EdgeLineSegment = {
...edgeLineSegment,
points: [ points: [
applyMatrix3(startPoint, isometricTransformMatrix), applyMatrix3(startPoint, isometricTransformMatrix),
applyMatrix3(endPoint, isometricTransformMatrix), applyMatrix3(endPoint, isometricTransformMatrix),
], ],
}; };
if (metadata) transformedSegment.metadata = metadata;
transformedEdgeLineSegments.push(transformedSegment); transformedEdgeLineSegments.push(transformedSegment);
} }
@ -530,6 +559,96 @@ export const processNodePositionsAndEdgeLineSegments = createSelector(
} }
); );
const indexedProcessNodePositionsAndEdgeLineSegments = createSelector(
processNodePositionsAndEdgeLineSegments,
function visibleProcessNodePositionsAndEdgeLineSegments({
/* eslint-disable no-shadow */
processNodePositions,
edgeLineSegments,
/* eslint-enable no-shadow */
}) {
const tree: rbush<IndexedEntity> = new rbush();
const processesToIndex: IndexedProcessNode[] = [];
const edgeLineSegmentsToIndex: IndexedEdgeLineSegment[] = [];
// Make sure these numbers are big enough to cover the process nodes at all zoom levels.
// The process nodes don't extend equally in all directions from their center point.
const processNodeViewWidth = 720;
const processNodeViewHeight = 240;
const lineSegmentPadding = 30;
for (const [processEvent, position] of processNodePositions) {
const [nodeX, nodeY] = position;
const indexedEvent: IndexedProcessNode = {
minX: nodeX - 0.5 * processNodeViewWidth,
minY: nodeY - 0.5 * processNodeViewHeight,
maxX: nodeX + 0.5 * processNodeViewWidth,
maxY: nodeY + 0.5 * processNodeViewHeight,
position,
entity: processEvent,
type: 'processNode',
};
processesToIndex.push(indexedEvent);
}
for (const edgeLineSegment of edgeLineSegments) {
const {
points: [[x1, y1], [x2, y2]],
} = edgeLineSegment;
const indexedLineSegment: IndexedEdgeLineSegment = {
minX: Math.min(x1, x2) - lineSegmentPadding,
minY: Math.min(y1, y2) - lineSegmentPadding,
maxX: Math.max(x1, x2) + lineSegmentPadding,
maxY: Math.max(y1, y2) + lineSegmentPadding,
entity: edgeLineSegment,
type: 'edgeLine',
};
edgeLineSegmentsToIndex.push(indexedLineSegment);
}
tree.load([...processesToIndex, ...edgeLineSegmentsToIndex]);
return tree;
}
);
export const visibleProcessNodePositionsAndEdgeLineSegments = createSelector(
indexedProcessNodePositionsAndEdgeLineSegments,
function visibleProcessNodePositionsAndEdgeLineSegments(tree) {
// memoize the results of this call to avoid unnecessarily rerunning
let lastBoundingBox: AABB | null = null;
let currentlyVisible: VisibleEntites = {
processNodePositions: new Map<ResolverEvent, Vector2>(),
connectingEdgeLineSegments: [],
};
return (boundingBox: AABB) => {
if (lastBoundingBox !== null && isEqual(lastBoundingBox, boundingBox)) {
return currentlyVisible;
} else {
const {
minimum: [minX, minY],
maximum: [maxX, maxY],
} = boundingBox;
const entities = tree.search({
minX,
minY,
maxX,
maxY,
});
const visibleProcessNodePositions = new Map<ResolverEvent, Vector2>(
entities
.filter((entity): entity is IndexedProcessNode => entity.type === 'processNode')
.map((node) => [node.entity, node.position])
);
const connectingEdgeLineSegments = entities
.filter((entity): entity is IndexedEdgeLineSegment => entity.type === 'edgeLine')
.map((node) => node.entity);
currentlyVisible = {
processNodePositions: visibleProcessNodePositions,
connectingEdgeLineSegments,
};
lastBoundingBox = boundingBox;
return currentlyVisible;
}
};
}
);
/** /**
* Returns the `children` and `ancestors` limits for the current graph, if any. * Returns the `children` and `ancestors` limits for the current graph, if any.
* *

View file

@ -0,0 +1,165 @@
/*
* 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 { ResolverAction } from '../actions';
import { resolverReducer } from '../reducer';
import { ResolverState } from '../../types';
import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/types';
import { visibleProcessNodePositionsAndEdgeLineSegments } from '../selectors';
import { mockProcessEvent } from '../../models/process_event_test_helpers';
describe('resolver visible entities', () => {
let processA: LegacyEndpointEvent;
let processB: LegacyEndpointEvent;
let processC: LegacyEndpointEvent;
let processD: LegacyEndpointEvent;
let processE: LegacyEndpointEvent;
let processF: LegacyEndpointEvent;
let processG: LegacyEndpointEvent;
let store: Store<ResolverState, ResolverAction>;
beforeEach(() => {
/*
* A
* |
* B
* |
* C
* |
* D etc
*/
processA = mockProcessEvent({
endgame: {
process_name: '',
event_type_full: 'process_event',
event_subtype_full: 'creation_event',
unique_pid: 0,
},
});
processB = mockProcessEvent({
endgame: {
event_type_full: 'process_event',
event_subtype_full: 'already_running',
unique_pid: 1,
unique_ppid: 0,
},
});
processC = mockProcessEvent({
endgame: {
event_type_full: 'process_event',
event_subtype_full: 'creation_event',
unique_pid: 2,
unique_ppid: 1,
},
});
processD = mockProcessEvent({
endgame: {
event_type_full: 'process_event',
event_subtype_full: 'creation_event',
unique_pid: 3,
unique_ppid: 2,
},
});
processE = mockProcessEvent({
endgame: {
event_type_full: 'process_event',
event_subtype_full: 'creation_event',
unique_pid: 4,
unique_ppid: 3,
},
});
processF = mockProcessEvent({
endgame: {
event_type_full: 'process_event',
event_subtype_full: 'creation_event',
unique_pid: 5,
unique_ppid: 4,
},
});
processF = mockProcessEvent({
endgame: {
event_type_full: 'process_event',
event_subtype_full: 'creation_event',
unique_pid: 6,
unique_ppid: 5,
},
});
processG = mockProcessEvent({
endgame: {
event_type_full: 'process_event',
event_subtype_full: 'creation_event',
unique_pid: 7,
unique_ppid: 6,
},
});
store = createStore(resolverReducer, undefined);
});
describe('when rendering a large tree with a small viewport', () => {
beforeEach(() => {
const events: ResolverEvent[] = [
processA,
processB,
processC,
processD,
processE,
processF,
processG,
];
const action: ResolverAction = {
type: 'serverReturnedResolverData',
payload: { events, stats: new Map(), lineageLimits: { children: '', ancestors: '' } },
};
const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [300, 200] };
store.dispatch(action);
store.dispatch(cameraAction);
});
it('the visibleProcessNodePositions list should only include 2 nodes', () => {
const { processNodePositions } = visibleProcessNodePositionsAndEdgeLineSegments(
store.getState()
)(0);
expect([...processNodePositions.keys()].length).toEqual(2);
});
it('the visibleEdgeLineSegments list should only include one edge line', () => {
const { connectingEdgeLineSegments } = visibleProcessNodePositionsAndEdgeLineSegments(
store.getState()
)(0);
expect(connectingEdgeLineSegments.length).toEqual(1);
});
});
describe('when rendering a large tree with a large viewport', () => {
beforeEach(() => {
const events: ResolverEvent[] = [
processA,
processB,
processC,
processD,
processE,
processF,
processG,
];
const action: ResolverAction = {
type: 'serverReturnedResolverData',
payload: { events, stats: new Map(), lineageLimits: { children: '', ancestors: '' } },
};
const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [2000, 2000] };
store.dispatch(action);
store.dispatch(cameraAction);
});
it('the visibleProcessNodePositions list should include all process nodes', () => {
const { processNodePositions } = visibleProcessNodePositionsAndEdgeLineSegments(
store.getState()
)(0);
expect([...processNodePositions.keys()].length).toEqual(5);
});
it('the visibleEdgeLineSegments list include all lines', () => {
const { connectingEdgeLineSegments } = visibleProcessNodePositionsAndEdgeLineSegments(
store.getState()
)(0);
expect(connectingEdgeLineSegments.length).toEqual(4);
});
});
});

View file

@ -112,7 +112,6 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => {
query: { events: 100 }, query: { events: 100 },
} }
); );
api.dispatch({ api.dispatch({
type: 'serverReturnedRelatedEventData', type: 'serverReturnedRelatedEventData',
payload: result, payload: result,

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { createSelector } from 'reselect';
import * as cameraSelectors from './camera/selectors'; import * as cameraSelectors from './camera/selectors';
import * as dataSelectors from './data/selectors'; import * as dataSelectors from './data/selectors';
import * as uiSelectors from './ui/selectors'; import * as uiSelectors from './ui/selectors';
@ -60,6 +61,11 @@ export const processAdjacencies = composeSelectors(
dataSelectors.processAdjacencies dataSelectors.processAdjacencies
); );
export const terminatedProcesses = composeSelectors(
dataStateSelector,
dataSelectors.terminatedProcesses
);
/** /**
* Returns a map of `ResolverEvent` entity_id to their related event and alert statistics * Returns a map of `ResolverEvent` entity_id to their related event and alert statistics
*/ */
@ -171,3 +177,27 @@ function composeSelectors<OuterState, InnerState, ReturnValue>(
): (state: OuterState) => ReturnValue { ): (state: OuterState) => ReturnValue {
return (state) => secondSelector(selector(state)); return (state) => secondSelector(selector(state));
} }
const boundingBox = composeSelectors(cameraStateSelector, cameraSelectors.viewableBoundingBox);
const indexedProcessNodesAndEdgeLineSegments = composeSelectors(
dataStateSelector,
dataSelectors.visibleProcessNodePositionsAndEdgeLineSegments
);
/**
* Return the visible edge lines and process nodes based on the camera position at `time`.
* The bounding box represents what the camera can see. The camera position is a function of time because it can be
* animated. So in order to get the currently visible entities, we need to pass in time.
*/
export const visibleProcessNodePositionsAndEdgeLineSegments = createSelector(
indexedProcessNodesAndEdgeLineSegments,
boundingBox,
function (
/* eslint-disable no-shadow */
indexedProcessNodesAndEdgeLineSegments,
boundingBox
/* eslint-enable no-shadow */
) {
return (time: number) => indexedProcessNodesAndEdgeLineSegments(boundingBox(time));
}
);

View file

@ -5,7 +5,7 @@
*/ */
import { Store } from 'redux'; import { Store } from 'redux';
import { BBox } from 'rbush';
import { ResolverAction } from './store/actions'; import { ResolverAction } from './store/actions';
export { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions';
import { import {
@ -142,6 +142,36 @@ export type CameraState = {
} }
); );
/**
* Wrappers around our internal types that make them compatible with `rbush`.
*/
export type IndexedEntity = IndexedEdgeLineSegment | IndexedProcessNode;
/**
* The entity stored in rbush for resolver edge lines.
*/
export interface IndexedEdgeLineSegment extends BBox {
type: 'edgeLine';
entity: EdgeLineSegment;
}
/**
* The entity store in rbush for resolver process nodes.
*/
export interface IndexedProcessNode extends BBox {
type: 'processNode';
entity: ResolverEvent;
position: Vector2;
}
/**
* A type containing all things to actually be rendered to the DOM.
*/
export interface VisibleEntites {
processNodePositions: ProcessPositions;
connectingEdgeLineSegments: EdgeLineSegment[];
}
/** /**
* State for `data` reducer which handles receiving Resolver data from the backend. * State for `data` reducer which handles receiving Resolver data from the backend.
*/ */
@ -287,6 +317,8 @@ export interface DurationDetails {
*/ */
export interface EdgeLineMetadata { export interface EdgeLineMetadata {
elapsedTime?: DurationDetails; elapsedTime?: DurationDetails;
// A string of the two joined process nodes concatted together.
uniqueId: string;
} }
/** /**
* A tuple of 2 vector2 points forming a polyline. Used to connect process nodes in the graph. * A tuple of 2 vector2 points forming a polyline. Used to connect process nodes in the graph.
@ -298,7 +330,7 @@ export type EdgeLinePoints = Vector2[];
*/ */
export interface EdgeLineSegment { export interface EdgeLineSegment {
points: EdgeLinePoints; points: EdgeLinePoints;
metadata?: EdgeLineMetadata; metadata: EdgeLineMetadata;
} }
/** /**

View file

@ -12,8 +12,6 @@ import styled from 'styled-components';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useUiSetting } from '../../common/lib/kibana'; import { useUiSetting } from '../../common/lib/kibana';
import { DEFAULT_DARK_MODE } from '../../../common/constants'; import { DEFAULT_DARK_MODE } from '../../../common/constants';
import { ResolverEvent } from '../../../common/endpoint/types';
import * as processModel from '../models/process_event';
import { ResolverProcessType } from '../types'; import { ResolverProcessType } from '../types';
type ResolverColorNames = type ResolverColorNames =
@ -417,27 +415,13 @@ const processTypeToCube: Record<ResolverProcessType, keyof NodeStyleMap> = {
unknownEvent: 'runningProcessCube', unknownEvent: 'runningProcessCube',
}; };
/**
* This will return which type the ResolverEvent will display as in the Node component
* it will be something like 'runningProcessCube' or 'terminatedProcessCube'
*
* @param processEvent {ResolverEvent} the event to get the Resolver Component Node type of
*/
export function nodeType(processEvent: ResolverEvent): keyof NodeStyleMap {
const processType = processModel.eventType(processEvent);
if (processType in processTypeToCube) {
return processTypeToCube[processType];
}
return 'runningProcessCube';
}
/** /**
* A hook to bring Resolver theming information into components. * A hook to bring Resolver theming information into components.
*/ */
export const useResolverTheme = (): { export const useResolverTheme = (): {
colorMap: ColorMap; colorMap: ColorMap;
nodeAssets: NodeStyleMap; nodeAssets: NodeStyleMap;
cubeAssetsForNode: (arg0: ResolverEvent) => NodeStyleConfig; cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessOrigin: boolean) => NodeStyleConfig;
} => { } => {
const isDarkMode = useUiSetting<boolean>(DEFAULT_DARK_MODE); const isDarkMode = useUiSetting<boolean>(DEFAULT_DARK_MODE);
const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight;
@ -511,12 +495,14 @@ export const useResolverTheme = (): {
}, },
}; };
/** function cubeAssetsForNode(isProcessTerminated: boolean, isProcessOrigin: boolean) {
* Export assets to reuse symbols/icons in other places in the app (e.g. tables, etc.) if (isProcessTerminated) {
* @param processEvent : The process event to fetch node assets for return nodeAssets[processTypeToCube.processTerminated];
*/ } else if (isProcessOrigin) {
function cubeAssetsForNode(processEvent: ResolverEvent) { return nodeAssets[processTypeToCube.processCausedAlert];
return nodeAssets[nodeType(processEvent)]; } else {
return nodeAssets[processTypeToCube.processRan];
}
} }
return { colorMap, nodeAssets, cubeAssetsForNode }; return { colorMap, nodeAssets, cubeAssetsForNode };

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import React, { useLayoutEffect } from 'react'; import React, { useLayoutEffect, useContext } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { EuiLoadingSpinner } from '@elastic/eui'; import { EuiLoadingSpinner } from '@elastic/eui';
@ -16,9 +16,10 @@ import { GraphControls } from './graph_controls';
import { ProcessEventDot } from './process_event_dot'; import { ProcessEventDot } from './process_event_dot';
import { useCamera } from './use_camera'; import { useCamera } from './use_camera';
import { SymbolDefinitions, useResolverTheme } from './assets'; import { SymbolDefinitions, useResolverTheme } from './assets';
import { entityId } from '../../../common/endpoint/models/event';
import { ResolverAction } from '../types'; import { ResolverAction } from '../types';
import { ResolverEvent } from '../../../common/endpoint/types'; import { ResolverEvent } from '../../../common/endpoint/types';
import * as eventModel from '../../../common/endpoint/models/event'; import { SideEffectContext } from './side_effect_context';
interface StyledResolver { interface StyledResolver {
backgroundColor: string; backgroundColor: string;
@ -74,17 +75,20 @@ export const Resolver = React.memo(function Resolver({
className?: string; className?: string;
selectedEvent?: ResolverEvent; selectedEvent?: ResolverEvent;
}) { }) {
const { processNodePositions, edgeLineSegments } = useSelector( const { timestamp } = useContext(SideEffectContext);
selectors.processNodePositionsAndEdgeLineSegments
); const { processNodePositions, connectingEdgeLineSegments } = useSelector(
selectors.visibleProcessNodePositionsAndEdgeLineSegments
)(timestamp());
const dispatch: (action: ResolverAction) => unknown = useDispatch(); const dispatch: (action: ResolverAction) => unknown = useDispatch();
const { processToAdjacencyMap } = useSelector(selectors.processAdjacencies); const { processToAdjacencyMap } = useSelector(selectors.processAdjacencies);
const relatedEventsStats = useSelector(selectors.relatedEventsStats);
const { projectionMatrix, ref, onMouseDown } = useCamera(); const { projectionMatrix, ref, onMouseDown } = useCamera();
const isLoading = useSelector(selectors.isLoading); const isLoading = useSelector(selectors.isLoading);
const hasError = useSelector(selectors.hasError); const hasError = useSelector(selectors.hasError);
const relatedEventsStats = useSelector(selectors.relatedEventsStats);
const activeDescendantId = useSelector(selectors.uiActiveDescendantId); const activeDescendantId = useSelector(selectors.uiActiveDescendantId);
const terminatedProcesses = useSelector(selectors.terminatedProcesses);
const { colorMap } = useResolverTheme(); const { colorMap } = useResolverTheme();
useLayoutEffect(() => { useLayoutEffect(() => {
@ -123,10 +127,10 @@ export const Resolver = React.memo(function Resolver({
tabIndex={0} tabIndex={0}
aria-activedescendant={activeDescendantId || undefined} aria-activedescendant={activeDescendantId || undefined}
> >
{edgeLineSegments.map(({ points: [startPosition, endPosition], metadata }, index) => ( {connectingEdgeLineSegments.map(({ points: [startPosition, endPosition], metadata }) => (
<EdgeLine <EdgeLine
edgeLineMetadata={metadata} edgeLineMetadata={metadata}
key={index} key={metadata.uniqueId}
startPosition={startPosition} startPosition={startPosition}
endPosition={endPosition} endPosition={endPosition}
projectionMatrix={projectionMatrix} projectionMatrix={projectionMatrix}
@ -134,6 +138,7 @@ export const Resolver = React.memo(function Resolver({
))} ))}
{[...processNodePositions].map(([processEvent, position], index) => { {[...processNodePositions].map(([processEvent, position], index) => {
const adjacentNodeMap = processToAdjacencyMap.get(processEvent); const adjacentNodeMap = processToAdjacencyMap.get(processEvent);
const processEntityId = entityId(processEvent);
if (!adjacentNodeMap) { if (!adjacentNodeMap) {
// This should never happen // This should never happen
throw new Error('Issue calculating adjacency node map.'); throw new Error('Issue calculating adjacency node map.');
@ -145,7 +150,9 @@ export const Resolver = React.memo(function Resolver({
projectionMatrix={projectionMatrix} projectionMatrix={projectionMatrix}
event={processEvent} event={processEvent}
adjacentNodeMap={adjacentNodeMap} adjacentNodeMap={adjacentNodeMap}
relatedEventsStats={relatedEventsStats.get(eventModel.entityId(processEvent))} relatedEventsStats={relatedEventsStats.get(entityId(processEvent))}
isProcessTerminated={terminatedProcesses.has(processEntityId)}
isProcessOrigin={false}
/> />
); );
})} })}

View file

@ -51,6 +51,7 @@ const PanelContent = memo(function PanelContent() {
const urlSearch = history.location.search; const urlSearch = history.location.search;
const dispatch = useResolverDispatch(); const dispatch = useResolverDispatch();
const { timestamp } = useContext(SideEffectContext);
const queryParams: CrumbInfo = useMemo(() => { const queryParams: CrumbInfo = useMemo(() => {
return { crumbId: '', crumbEvent: '', ...querystring.parse(urlSearch.slice(1)) }; return { crumbId: '', crumbEvent: '', ...querystring.parse(urlSearch.slice(1)) };
}, [urlSearch]); }, [urlSearch]);
@ -84,7 +85,7 @@ const PanelContent = memo(function PanelContent() {
const paramsSelectedEvent = useMemo(() => { const paramsSelectedEvent = useMemo(() => {
return graphableProcesses.find((evt) => event.entityId(evt) === idFromParams); return graphableProcesses.find((evt) => event.entityId(evt) === idFromParams);
}, [graphableProcesses, idFromParams]); }, [graphableProcesses, idFromParams]);
const { timestamp } = useContext(SideEffectContext);
const [lastUpdatedProcess, setLastUpdatedProcess] = useState<null | ResolverEvent>(null); const [lastUpdatedProcess, setLastUpdatedProcess] = useState<null | ResolverEvent>(null);
/** /**
@ -218,11 +219,19 @@ const PanelContent = memo(function PanelContent() {
}, [panelToShow, dispatch]); }, [panelToShow, dispatch]);
const currentPanelView = useSelector(selectors.currentPanelView); const currentPanelView = useSelector(selectors.currentPanelView);
const terminatedProcesses = useSelector(selectors.terminatedProcesses);
const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined;
const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false;
const panelInstance = useMemo(() => { const panelInstance = useMemo(() => {
if (currentPanelView === 'processDetails') { if (currentPanelView === 'processDetails') {
return ( return (
<ProcessDetails processEvent={uiSelectedEvent!} pushToQueryParams={pushToQueryParams} /> <ProcessDetails
processEvent={uiSelectedEvent!}
pushToQueryParams={pushToQueryParams}
isProcessTerminated={isProcessTerminated}
isProcessOrigin={false}
/>
); );
} }
@ -261,7 +270,13 @@ const PanelContent = memo(function PanelContent() {
); );
} }
// The default 'Event List' / 'List of all processes' view // The default 'Event List' / 'List of all processes' view
return <ProcessListWithCounts pushToQueryParams={pushToQueryParams} />; return (
<ProcessListWithCounts
pushToQueryParams={pushToQueryParams}
isProcessTerminated={isProcessTerminated}
isProcessOrigin={false}
/>
);
}, [ }, [
uiSelectedEvent, uiSelectedEvent,
crumbEvent, crumbEvent,
@ -269,6 +284,7 @@ const PanelContent = memo(function PanelContent() {
pushToQueryParams, pushToQueryParams,
relatedStatsForIdFromParams, relatedStatsForIdFromParams,
currentPanelView, currentPanelView,
isProcessTerminated,
]); ]);
return <>{panelInstance}</>; return <>{panelInstance}</>;

View file

@ -41,10 +41,14 @@ const StyledDescriptionList = styled(EuiDescriptionList)`
*/ */
export const ProcessDetails = memo(function ProcessDetails({ export const ProcessDetails = memo(function ProcessDetails({
processEvent, processEvent,
isProcessTerminated,
isProcessOrigin,
pushToQueryParams, pushToQueryParams,
}: { }: {
processEvent: ResolverEvent; processEvent: ResolverEvent;
pushToQueryParams: (arg0: CrumbInfo) => unknown; isProcessTerminated: boolean;
isProcessOrigin: boolean;
pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
}) { }) {
const processName = event.eventName(processEvent); const processName = event.eventName(processEvent);
const processInfoEntry = useMemo(() => { const processInfoEntry = useMemo(() => {
@ -178,8 +182,8 @@ export const ProcessDetails = memo(function ProcessDetails({
if (!processEvent) { if (!processEvent) {
return { descriptionText: '' }; return { descriptionText: '' };
} }
return cubeAssetsForNode(processEvent); return cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
}, [processEvent, cubeAssetsForNode]); }, [processEvent, cubeAssetsForNode, isProcessTerminated, isProcessOrigin]);
const titleId = useMemo(() => htmlIdGenerator('resolverTable')(), []); const titleId = useMemo(() => htmlIdGenerator('resolverTable')(), []);
return ( return (
@ -188,7 +192,10 @@ export const ProcessDetails = memo(function ProcessDetails({
<EuiSpacer size="l" /> <EuiSpacer size="l" />
<EuiTitle size="xs"> <EuiTitle size="xs">
<h4 aria-describedby={titleId}> <h4 aria-describedby={titleId}>
<CubeForProcess processEvent={processEvent} /> <CubeForProcess
isProcessTerminated={isProcessTerminated}
isProcessOrigin={isProcessOrigin}
/>
{processName} {processName}
</h4> </h4>
</EuiTitle> </EuiTitle>

View file

@ -28,8 +28,12 @@ import { ResolverEvent } from '../../../../common/endpoint/types';
*/ */
export const ProcessListWithCounts = memo(function ProcessListWithCounts({ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
pushToQueryParams, pushToQueryParams,
isProcessTerminated,
isProcessOrigin,
}: { }: {
pushToQueryParams: (arg0: CrumbInfo) => unknown; pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
isProcessTerminated: boolean;
isProcessOrigin: boolean;
}) { }) {
interface ProcessTableView { interface ProcessTableView {
name: string; name: string;
@ -82,7 +86,10 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
pushToQueryParams({ crumbId: event.entityId(item.event), crumbEvent: '' }); pushToQueryParams({ crumbId: event.entityId(item.event), crumbEvent: '' });
}} }}
> >
<CubeForProcess processEvent={item.event} /> <CubeForProcess
isProcessTerminated={isProcessTerminated}
isProcessOrigin={isProcessOrigin}
/>
{name} {name}
</EuiButtonEmpty> </EuiButtonEmpty>
); );
@ -114,7 +121,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
}, },
}, },
], ],
[pushToQueryParams, handleBringIntoViewClick] [pushToQueryParams, handleBringIntoViewClick, isProcessOrigin, isProcessTerminated]
); );
const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments);

View file

@ -30,7 +30,7 @@ export const EventCountsForProcess = memo(function EventCountsForProcess({
relatedStats, relatedStats,
}: { }: {
processEvent: ResolverEvent; processEvent: ResolverEvent;
pushToQueryParams: (arg0: CrumbInfo) => unknown; pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
relatedStats: ResolverNodeStats; relatedStats: ResolverNodeStats;
}) { }) {
interface EventCountsTableView { interface EventCountsTableView {

View file

@ -96,7 +96,7 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({
}: { }: {
relatedEventId: string; relatedEventId: string;
parentEvent: ResolverEvent; parentEvent: ResolverEvent;
pushToQueryParams: (arg0: CrumbInfo) => unknown; pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
countForParent: number | undefined; countForParent: number | undefined;
}) { }) {
const processName = (parentEvent && event.eventName(parentEvent)) || '*'; const processName = (parentEvent && event.eventName(parentEvent)) || '*';

View file

@ -5,7 +5,6 @@
*/ */
import React, { memo } from 'react'; import React, { memo } from 'react';
import { ResolverEvent } from '../../../../common/endpoint/types';
import { useResolverTheme } from '../assets'; import { useResolverTheme } from '../assets';
/** /**
@ -13,12 +12,14 @@ import { useResolverTheme } from '../assets';
* Nodes on the graph and what's in the table. Using the same symbol in both places (as below) could help with that. * Nodes on the graph and what's in the table. Using the same symbol in both places (as below) could help with that.
*/ */
export const CubeForProcess = memo(function CubeForProcess({ export const CubeForProcess = memo(function CubeForProcess({
processEvent, isProcessTerminated,
isProcessOrigin,
}: { }: {
processEvent: ResolverEvent; isProcessTerminated: boolean;
isProcessOrigin: boolean;
}) { }) {
const { cubeAssetsForNode } = useResolverTheme(); const { cubeAssetsForNode } = useResolverTheme();
const { cubeSymbol, descriptionText } = cubeAssetsForNode(processEvent); const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
return ( return (
<> <>

View file

@ -15,7 +15,7 @@ import querystring from 'querystring';
import { NodeSubMenu, subMenuAssets } from './submenu'; import { NodeSubMenu, subMenuAssets } from './submenu';
import { applyMatrix3 } from '../lib/vector2'; import { applyMatrix3 } from '../lib/vector2';
import { Vector2, Matrix3, AdjacentProcessMap } from '../types'; import { Vector2, Matrix3, AdjacentProcessMap } from '../types';
import { SymbolIds, useResolverTheme, calculateResolverFontSize, nodeType } from './assets'; import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets';
import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types';
import { useResolverDispatch } from './use_resolver_dispatch'; import { useResolverDispatch } from './use_resolver_dispatch';
import * as eventModel from '../../../common/endpoint/models/event'; import * as eventModel from '../../../common/endpoint/models/event';
@ -239,6 +239,8 @@ const ProcessEventDotComponents = React.memo(
event, event,
projectionMatrix, projectionMatrix,
adjacentNodeMap, adjacentNodeMap,
isProcessTerminated,
isProcessOrigin,
relatedEventsStats, relatedEventsStats,
}: { }: {
/** /**
@ -262,6 +264,16 @@ const ProcessEventDotComponents = React.memo(
*/ */
adjacentNodeMap: AdjacentProcessMap; adjacentNodeMap: AdjacentProcessMap;
/** /**
* Whether or not to show the process as terminated.
*/
isProcessTerminated: boolean;
/**
* Whether or not to show the process as the originating event.
*/
isProcessOrigin: boolean;
/**
* A collection of events related to the current node and statistics (e.g. counts indexed by event type)
* to provide the user some visibility regarding the contents thereof.
* Statistics for the number of related events and alerts for this process node * Statistics for the number of related events and alerts for this process node
*/ */
relatedEventsStats?: ResolverNodeStats; relatedEventsStats?: ResolverNodeStats;
@ -363,7 +375,7 @@ const ProcessEventDotComponents = React.memo(
}) })
| null; | null;
} = React.createRef(); } = React.createRef();
const { colorMap, nodeAssets } = useResolverTheme(); const { colorMap, cubeAssetsForNode } = useResolverTheme();
const { const {
backingFill, backingFill,
cubeSymbol, cubeSymbol,
@ -371,7 +383,8 @@ const ProcessEventDotComponents = React.memo(
isLabelFilled, isLabelFilled,
labelButtonFill, labelButtonFill,
strokeColor, strokeColor,
} = nodeAssets[nodeType(event)]; } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []); const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []);
const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [ const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [

View file

@ -5616,6 +5616,11 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
"@types/rbush@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-3.0.0.tgz#b6887d99b159e87ae23cd14eceff34f139842aa6"
integrity sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg==
"@types/reach__router@^1.2.3", "@types/reach__router@^1.2.6": "@types/reach__router@^1.2.3", "@types/reach__router@^1.2.6":
version "1.2.6" version "1.2.6"
resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.2.6.tgz#b14cf1adbd1a365d204bbf6605cd9dd7b8816c87" resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.2.6.tgz#b14cf1adbd1a365d204bbf6605cd9dd7b8816c87"
@ -25291,6 +25296,13 @@ raw-loader@~0.5.1:
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= integrity sha1-DD0L6u2KAclm2Xh793goElKpeao=
rbush@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf"
integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==
dependencies:
quickselect "^2.0.0"
rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"