[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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
);
|
||||
|
||||
api.dispatch({
|
||||
type: 'serverReturnedRelatedEventData',
|
||||
payload: result,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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}</>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)) || '*';
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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), [
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue