Resolver refactoring (#70312)

* remove unused piece of state
* Move related event total calculation to selector
* rename xScale
* remove `let`
* Move `dispatch` call out of HTTP try-catch
This commit is contained in:
Robert Austin 2020-06-30 17:32:44 -04:00 committed by GitHub
parent 8903d3427e
commit 893525c74c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 118 additions and 90 deletions

View file

@ -9,7 +9,6 @@ import { DataState } from '../../types';
import { ResolverAction } from '../actions';
const initialState: DataState = {
relatedEventsStats: new Map(),
relatedEvents: new Map(),
relatedEventsReady: new Map(),
};

View file

@ -301,3 +301,46 @@ export function databaseDocumentIDToAbort(state: DataState): string | null {
return null;
}
}
/**
* `ResolverNodeStats` for a process (`ResolverEvent`)
*/
const relatedEventStatsForProcess: (
state: DataState
) => (event: ResolverEvent) => ResolverNodeStats | null = createSelector(
relatedEventsStats,
(statsMap) => {
if (!statsMap) {
return () => null;
}
return (event: ResolverEvent) => {
const nodeStats = statsMap.get(uniquePidForProcess(event));
if (!nodeStats) {
return null;
}
return nodeStats;
};
}
);
/**
* The sum of all related event categories for a process.
*/
export const relatedEventTotalForProcess: (
state: DataState
) => (event: ResolverEvent) => number | null = createSelector(
relatedEventStatsForProcess,
(statsForProcess) => {
return (event: ResolverEvent) => {
const stats = statsForProcess(event);
if (!stats) {
return null;
}
let total = 0;
for (const value of Object.values(stats.events.byCategory)) {
total += value;
}
return total;
};
}
);

View file

@ -43,7 +43,7 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => {
action.type === 'appDetectedMissingEventData'
) {
const entityIdToFetchFor = action.payload;
let result: ResolverRelatedEvents;
let result: ResolverRelatedEvents | undefined;
try {
result = await context.services.http.get(
`/api/endpoint/resolver/${entityIdToFetchFor}/events`,
@ -51,17 +51,19 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => {
query: { events: 100 },
}
);
api.dispatch({
type: 'serverReturnedRelatedEventData',
payload: result,
});
} catch (e) {
} catch {
api.dispatch({
type: 'serverFailedToReturnRelatedEventData',
payload: action.payload,
});
}
if (result) {
api.dispatch({
type: 'serverReturnedRelatedEventData',
payload: result,
});
}
}
};
};

View file

@ -188,6 +188,14 @@ const indexedProcessNodesAndEdgeLineSegments = composeSelectors(
dataSelectors.visibleProcessNodePositionsAndEdgeLineSegments
);
/**
* Total count of related events for a process.
*/
export const relatedEventTotalForProcess = composeSelectors(
dataStateSelector,
dataSelectors.relatedEventTotalForProcess
);
/**
* 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

View file

@ -7,12 +7,7 @@
import { Store } from 'redux';
import { BBox } from 'rbush';
import { ResolverAction } from './store/actions';
import {
ResolverEvent,
ResolverNodeStats,
ResolverRelatedEvents,
ResolverTree,
} from '../../common/endpoint/types';
import { ResolverEvent, ResolverRelatedEvents, ResolverTree } from '../../common/endpoint/types';
/**
* Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`.
@ -176,7 +171,6 @@ export interface VisibleEntites {
* State for `data` reducer which handles receiving Resolver data from the backend.
*/
export interface DataState {
readonly relatedEventsStats: Map<string, ResolverNodeStats>;
readonly relatedEvents: Map<string, ResolverRelatedEvents>;
readonly relatedEventsReady: Map<string, boolean>;
/**

View file

@ -107,7 +107,7 @@ export const ResolverMap = React.memo(function ({
projectionMatrix={projectionMatrix}
event={processEvent}
adjacentNodeMap={adjacentNodeMap}
relatedEventsStats={
relatedEventsStatsForProcess={
relatedEventsStats ? relatedEventsStats.get(entityId(processEvent)) : undefined
}
isProcessTerminated={terminatedProcesses.has(processEntityId)}

View file

@ -4,14 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable react/display-name */
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { htmlIdGenerator, EuiButton, EuiI18nNumber, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { useSelector } from 'react-redux';
import { NodeSubMenu, subMenuAssets } from './submenu';
import { applyMatrix3 } from '../models/vector2';
import { Vector2, Matrix3, AdjacentProcessMap } from '../types';
@ -23,7 +25,7 @@ import * as selectors from '../store/selectors';
import { CrumbInfo } from './panels/panel_content_utilities';
/**
* A map of all known event types (in ugly schema format) to beautifully i18n'd display names
* A record of all known event types (in schema format) to translations
*/
export const displayNameRecord = {
application: i18n.translate(
@ -177,11 +179,11 @@ type EventDisplayName = typeof displayNameRecord[keyof typeof displayNameRecord]
typeof unknownEventTypeMessage;
/**
* Take a gross `schemaName` and return a beautiful translated one.
* Take a `schemaName` and return a translation.
*/
const getDisplayName: (schemaName: string) => EventDisplayName = function nameInSchemaToDisplayName(
schemaName
) {
const schemaNameTranslation: (
schemaName: string
) => EventDisplayName = function nameInSchemaToDisplayName(schemaName) {
if (schemaName in displayNameRecord) {
return displayNameRecord[schemaName as keyof typeof displayNameRecord];
}
@ -232,7 +234,7 @@ const StyledDescriptionText = styled.div<StyledDescriptionText>`
/**
* An artifact that represents a process node and the things associated with it in the Resolver
*/
const ProcessEventDotComponents = React.memo(
const UnstyledProcessEventDot = React.memo(
({
className,
position,
@ -241,7 +243,7 @@ const ProcessEventDotComponents = React.memo(
adjacentNodeMap,
isProcessTerminated,
isProcessOrigin,
relatedEventsStats,
relatedEventsStatsForProcess,
}: {
/**
* A `className` string provided by `styled`
@ -276,14 +278,14 @@ const ProcessEventDotComponents = React.memo(
* 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;
relatedEventsStatsForProcess?: ResolverNodeStats;
}) => {
/**
* Convert the position, which is in 'world' coordinates, to screen coordinates.
*/
const [left, top] = applyMatrix3(position, projectionMatrix);
const [magFactorX] = projectionMatrix;
const [xScale] = projectionMatrix;
// Node (html id=) IDs
const selfId = adjacentNodeMap.self;
@ -293,25 +295,14 @@ const ProcessEventDotComponents = React.memo(
// Entity ID of self
const selfEntityId = eventModel.entityId(event);
const isShowingEventActions = magFactorX > 0.8;
const isShowingDescriptionText = magFactorX >= 0.55;
const isShowingEventActions = xScale > 0.8;
const isShowingDescriptionText = xScale >= 0.55;
/**
* As the resolver zooms and buttons and text change visibility, we look to keep the overall container properly vertically aligned
*/
const actionalButtonsBaseTopOffset = 5;
let actionableButtonsTopOffset;
switch (true) {
case isShowingEventActions:
actionableButtonsTopOffset = actionalButtonsBaseTopOffset + 3.5 * magFactorX;
break;
case isShowingDescriptionText:
actionableButtonsTopOffset = actionalButtonsBaseTopOffset + magFactorX;
break;
default:
actionableButtonsTopOffset = actionalButtonsBaseTopOffset + 21 * magFactorX;
break;
}
const actionableButtonsTopOffset =
(isShowingEventActions ? 3.5 : isShowingDescriptionText ? 1 : 21) * xScale + 5;
/**
* The `left` and `top` values represent the 'center' point of the process node.
@ -326,26 +317,24 @@ const ProcessEventDotComponents = React.memo(
/**
* As the scale changes and button visibility toggles on the graph, these offsets help scale to keep the nodes centered on the edge
*/
const nodeXOffsetValue = isShowingEventActions
? -0.147413
: -0.147413 - (magFactorX - 0.5) * 0.08;
const nodeXOffsetValue = isShowingEventActions ? -0.147413 : -0.147413 - (xScale - 0.5) * 0.08;
const nodeYOffsetValue = isShowingEventActions
? -0.53684
: -0.53684 + (-magFactorX * 0.2 * (1 - magFactorX)) / magFactorX;
: -0.53684 + (-xScale * 0.2 * (1 - xScale)) / xScale;
const processNodeViewXOffset = nodeXOffsetValue * logicalProcessNodeViewWidth * magFactorX;
const processNodeViewYOffset = nodeYOffsetValue * logicalProcessNodeViewHeight * magFactorX;
const processNodeViewXOffset = nodeXOffsetValue * logicalProcessNodeViewWidth * xScale;
const processNodeViewYOffset = nodeYOffsetValue * logicalProcessNodeViewHeight * xScale;
const nodeViewportStyle = useMemo(
() => ({
left: `${left + processNodeViewXOffset}px`,
top: `${top + processNodeViewYOffset}px`,
// Width of symbol viewport scaled to fit
width: `${logicalProcessNodeViewWidth * magFactorX}px`,
width: `${logicalProcessNodeViewWidth * xScale}px`,
// Height according to symbol viewbox AR
height: `${logicalProcessNodeViewHeight * magFactorX}px`,
height: `${logicalProcessNodeViewHeight * xScale}px`,
}),
[left, magFactorX, processNodeViewXOffset, processNodeViewYOffset, top]
[left, xScale, processNodeViewXOffset, processNodeViewYOffset, top]
);
/**
@ -354,7 +343,7 @@ const ProcessEventDotComponents = React.memo(
* 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this.
* 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise
*/
const scaledTypeSize = calculateResolverFontSize(magFactorX, 18.75, 12.5);
const scaledTypeSize = calculateResolverFontSize(xScale, 18.75, 12.5);
const markerBaseSize = 15;
const markerSize = markerBaseSize;
@ -465,47 +454,42 @@ const ProcessEventDotComponents = React.memo(
* e.g. "10 DNS", "230 File"
*/
const [relatedEventOptions, grandTotal] = useMemo(() => {
const relatedEventOptions = useMemo(() => {
const relatedStatsList = [];
if (!relatedEventsStats) {
if (!relatedEventsStatsForProcess) {
// Return an empty set of options if there are no stats to report
return [[], 0];
return [];
}
let runningTotal = 0;
// If we have entries to show, map them into options to display in the selectable list
for (const category in relatedEventsStats.events.byCategory) {
if (Object.hasOwnProperty.call(relatedEventsStats.events.byCategory, category)) {
const total = relatedEventsStats.events.byCategory[category];
runningTotal += total;
const displayName = getDisplayName(category);
relatedStatsList.push({
prefix: <EuiI18nNumber value={total || 0} />,
optionTitle: `${displayName}`,
action: () => {
dispatch({
type: 'userSelectedRelatedEventCategory',
payload: {
subject: event,
category,
},
});
pushToQueryParams({ crumbId: selfEntityId, crumbEvent: category });
},
});
}
for (const [category, total] of Object.entries(
relatedEventsStatsForProcess.events.byCategory
)) {
relatedStatsList.push({
prefix: <EuiI18nNumber value={total || 0} />,
optionTitle: schemaNameTranslation(category),
action: () => {
dispatch({
type: 'userSelectedRelatedEventCategory',
payload: {
subject: event,
category,
},
});
pushToQueryParams({ crumbId: selfEntityId, crumbEvent: category });
},
});
}
return [relatedStatsList, runningTotal];
}, [relatedEventsStats, dispatch, event, pushToQueryParams, selfEntityId]);
return relatedStatsList;
}, [relatedEventsStatsForProcess, dispatch, event, pushToQueryParams, selfEntityId]);
const relatedEventStatusOrOptions = (() => {
if (!relatedEventsStats) {
return subMenuAssets.initialMenuStatus;
}
const relatedEventStatusOrOptions = !relatedEventsStatsForProcess
? subMenuAssets.initialMenuStatus
: relatedEventOptions;
return relatedEventOptions;
})();
const grandTotal: number | null = useSelector(selectors.relatedEventTotalForProcess)(event);
/* eslint-disable jsx-a11y/click-events-have-key-events */
/**
@ -586,7 +570,7 @@ const ProcessEventDotComponents = React.memo(
{descriptionText}
</StyledDescriptionText>
<div
className={magFactorX >= 2 ? 'euiButton' : 'euiButton euiButton--small'}
className={xScale >= 2 ? 'euiButton' : 'euiButton euiButton--small'}
data-test-subject="nodeLabel"
id={labelId}
onClick={handleClick}
@ -605,8 +589,8 @@ const ProcessEventDotComponents = React.memo(
id={labelId}
size="s"
style={{
maxHeight: `${Math.min(26 + magFactorX * 3, 32)}px`,
maxWidth: `${isShowingEventActions ? 400 : 210 * magFactorX}px`,
maxHeight: `${Math.min(26 + xScale * 3, 32)}px`,
maxWidth: `${isShowingEventActions ? 400 : 210 * xScale}px`,
}}
tabIndex={-1}
title={eventModel.eventName(event)}
@ -630,7 +614,7 @@ const ProcessEventDotComponents = React.memo(
}}
>
<EuiFlexItem grow={false} className="related-dropdown">
{grandTotal > 0 && (
{grandTotal !== null && grandTotal > 0 && (
<NodeSubMenu
count={grandTotal}
buttonBorderColor={labelButtonFill}
@ -649,9 +633,7 @@ const ProcessEventDotComponents = React.memo(
}
);
ProcessEventDotComponents.displayName = 'ProcessEventDot';
export const ProcessEventDot = styled(ProcessEventDotComponents)`
export const ProcessEventDot = styled(UnstyledProcessEventDot)`
position: absolute;
text-align: left;
font-size: 10px;