[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:
parent
f1a1178328
commit
9ebf41c77c
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
x-pack/plugins/security_solution/public/resolver/lib/aabb.ts
Normal file
14
x-pack/plugins/security_solution/public/resolver/lib/aabb.ts
Normal 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);
|
||||||
|
}
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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}</>;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)) || '*';
|
||||||
|
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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), [
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue