[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"
},
"dependencies": {
"@types/rbush": "^3.0.0",
"@types/seedrandom": ">=2.0.0 <4.0.0",
"lodash": "^4.17.15",
"querystring": "^0.2.0",
"redux-devtools-extension": "^2.13.8",
"@types/seedrandom": ">=2.0.0 <4.0.0"
"rbush": "^3.0.1",
"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];
}
/**
* 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
*/

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.
*/

View file

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

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import rbush from 'rbush';
import { createSelector } from 'reselect';
import {
DataState,
@ -16,11 +17,20 @@ import {
AdjacentProcessMap,
Vector2,
EdgeLineMetadata,
IndexedEntity,
IndexedEdgeLineSegment,
IndexedProcessNode,
AABB,
VisibleEntites,
} from '../../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 { isGraphableProcess, uniquePidForProcess } from '../../models/process_event';
import {
isGraphableProcess,
isTerminatedProcess,
uniquePidForProcess,
} from '../../models/process_event';
import {
factory as indexedProcessTreeFactory,
children as indexedProcessTreeChildren,
@ -29,6 +39,7 @@ import {
levelOrder,
} from '../../models/indexed_process_tree';
import { getFriendlyElapsedTime } from '../../lib/date';
import { isEqual } from '../../lib/aabb';
const unit = 140;
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
* 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[] {
const edgeLineSegments: EdgeLineSegment[] = [];
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
*/
@ -168,6 +193,9 @@ function processEdgeLineSegments(
const { process, parent, parentWidth } = metadata;
const position = positions.get(process);
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) {
/**
@ -176,12 +204,13 @@ function processEdgeLineSegments(
throw new Error();
}
const parentTime = eventTimestamp(parent);
const processTime = eventTimestamp(process);
const parentTime = event.eventTimestamp(parent);
const processTime = event.eventTimestamp(process);
if (parentTime && processTime) {
const elapsedTime = getFriendlyElapsedTime(parentTime, processTime);
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
@ -226,6 +255,7 @@ function processEdgeLineSegments(
const lineFromParentToMidwayLine: EdgeLineSegment = {
points: [parentPosition, [parentPosition[0], midwayY]],
metadata: { uniqueId: `parentToMid${edgeLineId}` },
};
const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2;
@ -246,6 +276,7 @@ function processEdgeLineSegments(
midwayY,
],
],
metadata: { uniqueId: `midway${edgeLineId}` },
};
edgeLineSegments.push(
@ -508,18 +539,16 @@ export const processNodePositionsAndEdgeLineSegments = createSelector(
for (const edgeLineSegment of edgeLineSegments) {
const {
points: [startPoint, endPoint],
metadata,
} = edgeLineSegment;
const transformedSegment: EdgeLineSegment = {
...edgeLineSegment,
points: [
applyMatrix3(startPoint, isometricTransformMatrix),
applyMatrix3(endPoint, isometricTransformMatrix),
],
};
if (metadata) transformedSegment.metadata = metadata;
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.
*

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 },
}
);
api.dispatch({
type: 'serverReturnedRelatedEventData',
payload: result,

View file

@ -4,6 +4,7 @@
* 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 dataSelectors from './data/selectors';
import * as uiSelectors from './ui/selectors';
@ -60,6 +61,11 @@ export const processAdjacencies = composeSelectors(
dataSelectors.processAdjacencies
);
export const terminatedProcesses = composeSelectors(
dataStateSelector,
dataSelectors.terminatedProcesses
);
/**
* 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 {
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 { BBox } from 'rbush';
import { ResolverAction } from './store/actions';
export { ResolverAction } from './store/actions';
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.
*/
@ -287,6 +317,8 @@ export interface DurationDetails {
*/
export interface EdgeLineMetadata {
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.
@ -298,7 +330,7 @@ export type EdgeLinePoints = Vector2[];
*/
export interface EdgeLineSegment {
points: EdgeLinePoints;
metadata?: EdgeLineMetadata;
metadata: EdgeLineMetadata;
}
/**

View file

@ -12,8 +12,6 @@ import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { useUiSetting } from '../../common/lib/kibana';
import { DEFAULT_DARK_MODE } from '../../../common/constants';
import { ResolverEvent } from '../../../common/endpoint/types';
import * as processModel from '../models/process_event';
import { ResolverProcessType } from '../types';
type ResolverColorNames =
@ -417,27 +415,13 @@ const processTypeToCube: Record<ResolverProcessType, keyof NodeStyleMap> = {
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.
*/
export const useResolverTheme = (): {
colorMap: ColorMap;
nodeAssets: NodeStyleMap;
cubeAssetsForNode: (arg0: ResolverEvent) => NodeStyleConfig;
cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessOrigin: boolean) => NodeStyleConfig;
} => {
const isDarkMode = useUiSetting<boolean>(DEFAULT_DARK_MODE);
const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight;
@ -511,12 +495,14 @@ export const useResolverTheme = (): {
},
};
/**
* Export assets to reuse symbols/icons in other places in the app (e.g. tables, etc.)
* @param processEvent : The process event to fetch node assets for
*/
function cubeAssetsForNode(processEvent: ResolverEvent) {
return nodeAssets[nodeType(processEvent)];
function cubeAssetsForNode(isProcessTerminated: boolean, isProcessOrigin: boolean) {
if (isProcessTerminated) {
return nodeAssets[processTypeToCube.processTerminated];
} else if (isProcessOrigin) {
return nodeAssets[processTypeToCube.processCausedAlert];
} else {
return nodeAssets[processTypeToCube.processRan];
}
}
return { colorMap, nodeAssets, cubeAssetsForNode };

View file

@ -4,7 +4,7 @@
* 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 styled from 'styled-components';
import { EuiLoadingSpinner } from '@elastic/eui';
@ -16,9 +16,10 @@ import { GraphControls } from './graph_controls';
import { ProcessEventDot } from './process_event_dot';
import { useCamera } from './use_camera';
import { SymbolDefinitions, useResolverTheme } from './assets';
import { entityId } from '../../../common/endpoint/models/event';
import { ResolverAction } from '../types';
import { ResolverEvent } from '../../../common/endpoint/types';
import * as eventModel from '../../../common/endpoint/models/event';
import { SideEffectContext } from './side_effect_context';
interface StyledResolver {
backgroundColor: string;
@ -74,17 +75,20 @@ export const Resolver = React.memo(function Resolver({
className?: string;
selectedEvent?: ResolverEvent;
}) {
const { processNodePositions, edgeLineSegments } = useSelector(
selectors.processNodePositionsAndEdgeLineSegments
);
const { timestamp } = useContext(SideEffectContext);
const { processNodePositions, connectingEdgeLineSegments } = useSelector(
selectors.visibleProcessNodePositionsAndEdgeLineSegments
)(timestamp());
const dispatch: (action: ResolverAction) => unknown = useDispatch();
const { processToAdjacencyMap } = useSelector(selectors.processAdjacencies);
const relatedEventsStats = useSelector(selectors.relatedEventsStats);
const { projectionMatrix, ref, onMouseDown } = useCamera();
const isLoading = useSelector(selectors.isLoading);
const hasError = useSelector(selectors.hasError);
const relatedEventsStats = useSelector(selectors.relatedEventsStats);
const activeDescendantId = useSelector(selectors.uiActiveDescendantId);
const terminatedProcesses = useSelector(selectors.terminatedProcesses);
const { colorMap } = useResolverTheme();
useLayoutEffect(() => {
@ -123,10 +127,10 @@ export const Resolver = React.memo(function Resolver({
tabIndex={0}
aria-activedescendant={activeDescendantId || undefined}
>
{edgeLineSegments.map(({ points: [startPosition, endPosition], metadata }, index) => (
{connectingEdgeLineSegments.map(({ points: [startPosition, endPosition], metadata }) => (
<EdgeLine
edgeLineMetadata={metadata}
key={index}
key={metadata.uniqueId}
startPosition={startPosition}
endPosition={endPosition}
projectionMatrix={projectionMatrix}
@ -134,6 +138,7 @@ export const Resolver = React.memo(function Resolver({
))}
{[...processNodePositions].map(([processEvent, position], index) => {
const adjacentNodeMap = processToAdjacencyMap.get(processEvent);
const processEntityId = entityId(processEvent);
if (!adjacentNodeMap) {
// This should never happen
throw new Error('Issue calculating adjacency node map.');
@ -145,7 +150,9 @@ export const Resolver = React.memo(function Resolver({
projectionMatrix={projectionMatrix}
event={processEvent}
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 dispatch = useResolverDispatch();
const { timestamp } = useContext(SideEffectContext);
const queryParams: CrumbInfo = useMemo(() => {
return { crumbId: '', crumbEvent: '', ...querystring.parse(urlSearch.slice(1)) };
}, [urlSearch]);
@ -84,7 +85,7 @@ const PanelContent = memo(function PanelContent() {
const paramsSelectedEvent = useMemo(() => {
return graphableProcesses.find((evt) => event.entityId(evt) === idFromParams);
}, [graphableProcesses, idFromParams]);
const { timestamp } = useContext(SideEffectContext);
const [lastUpdatedProcess, setLastUpdatedProcess] = useState<null | ResolverEvent>(null);
/**
@ -218,11 +219,19 @@ const PanelContent = memo(function PanelContent() {
}, [panelToShow, dispatch]);
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(() => {
if (currentPanelView === 'processDetails') {
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
return <ProcessListWithCounts pushToQueryParams={pushToQueryParams} />;
return (
<ProcessListWithCounts
pushToQueryParams={pushToQueryParams}
isProcessTerminated={isProcessTerminated}
isProcessOrigin={false}
/>
);
}, [
uiSelectedEvent,
crumbEvent,
@ -269,6 +284,7 @@ const PanelContent = memo(function PanelContent() {
pushToQueryParams,
relatedStatsForIdFromParams,
currentPanelView,
isProcessTerminated,
]);
return <>{panelInstance}</>;

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,6 @@
*/
import React, { memo } from 'react';
import { ResolverEvent } from '../../../../common/endpoint/types';
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.
*/
export const CubeForProcess = memo(function CubeForProcess({
processEvent,
isProcessTerminated,
isProcessOrigin,
}: {
processEvent: ResolverEvent;
isProcessTerminated: boolean;
isProcessOrigin: boolean;
}) {
const { cubeAssetsForNode } = useResolverTheme();
const { cubeSymbol, descriptionText } = cubeAssetsForNode(processEvent);
const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
return (
<>

View file

@ -15,7 +15,7 @@ import querystring from 'querystring';
import { NodeSubMenu, subMenuAssets } from './submenu';
import { applyMatrix3 } from '../lib/vector2';
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 { useResolverDispatch } from './use_resolver_dispatch';
import * as eventModel from '../../../common/endpoint/models/event';
@ -239,6 +239,8 @@ const ProcessEventDotComponents = React.memo(
event,
projectionMatrix,
adjacentNodeMap,
isProcessTerminated,
isProcessOrigin,
relatedEventsStats,
}: {
/**
@ -262,6 +264,16 @@ const ProcessEventDotComponents = React.memo(
*/
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
*/
relatedEventsStats?: ResolverNodeStats;
@ -363,7 +375,7 @@ const ProcessEventDotComponents = React.memo(
})
| null;
} = React.createRef();
const { colorMap, nodeAssets } = useResolverTheme();
const { colorMap, cubeAssetsForNode } = useResolverTheme();
const {
backingFill,
cubeSymbol,
@ -371,7 +383,8 @@ const ProcessEventDotComponents = React.memo(
isLabelFilled,
labelButtonFill,
strokeColor,
} = nodeAssets[nodeType(event)];
} = cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []);
const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [

View file

@ -5616,6 +5616,11 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
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":
version "1.2.6"
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"
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:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"