From 2e2550a9a63ef172117fbee9356402c3280bf338 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 30 Jun 2020 17:32:44 -0400 Subject: [PATCH] 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 --- .../public/resolver/store/data/reducer.ts | 1 - .../public/resolver/store/data/selectors.ts | 43 ++++++ .../public/resolver/store/middleware/index.ts | 14 +- .../public/resolver/store/selectors.ts | 8 ++ .../public/resolver/types.ts | 8 +- .../public/resolver/view/map.tsx | 2 +- .../resolver/view/process_event_dot.tsx | 132 ++++++++---------- 7 files changed, 118 insertions(+), 90 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index 45bf214005872..19b743374b8ed 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -9,7 +9,6 @@ import { DataState } from '../../types'; import { ResolverAction } from '../actions'; const initialState: DataState = { - relatedEventsStats: new Map(), relatedEvents: new Map(), relatedEventsReady: new Map(), }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index e45101e97e6c1..9c47c765457e3 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -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; + }; + } +); diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts index 194b50256c631..398e855a1f5d4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts @@ -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,16 +51,18 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { query: { events: 100 }, } ); + } catch { + api.dispatch({ + type: 'serverFailedToReturnRelatedEventData', + payload: action.payload, + }); + } + if (result) { api.dispatch({ type: 'serverReturnedRelatedEventData', payload: result, }); - } catch (e) { - api.dispatch({ - type: 'serverFailedToReturnRelatedEventData', - payload: action.payload, - }); } } }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 55e0072c5227f..e54193ab394a5 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -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 diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index fe5b2276603a8..5dd9a944b88ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -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; readonly relatedEvents: Map; readonly relatedEventsReady: Map; /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx index 9022932c1594f..3fc62fc318284 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx @@ -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)} diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 9df9ed84f3010..6442735abc8cd 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -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` /** * 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: , - optionTitle: `${displayName}`, - action: () => { - dispatch({ - type: 'userSelectedRelatedEventCategory', - payload: { - subject: event, - category, - }, - }); - - pushToQueryParams({ crumbId: selfEntityId, crumbEvent: category }); - }, - }); - } - } - return [relatedStatsList, runningTotal]; - }, [relatedEventsStats, dispatch, event, pushToQueryParams, selfEntityId]); - const relatedEventStatusOrOptions = (() => { - if (!relatedEventsStats) { - return subMenuAssets.initialMenuStatus; + for (const [category, total] of Object.entries( + relatedEventsStatsForProcess.events.byCategory + )) { + relatedStatsList.push({ + prefix: , + optionTitle: schemaNameTranslation(category), + action: () => { + dispatch({ + type: 'userSelectedRelatedEventCategory', + payload: { + subject: event, + category, + }, + }); + + pushToQueryParams({ crumbId: selfEntityId, crumbEvent: category }); + }, + }); } + return relatedStatsList; + }, [relatedEventsStatsForProcess, dispatch, event, pushToQueryParams, selfEntityId]); - return relatedEventOptions; - })(); + const relatedEventStatusOrOptions = !relatedEventsStatsForProcess + ? subMenuAssets.initialMenuStatus + : 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}
= 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( }} > - {grandTotal > 0 && ( + {grandTotal !== null && grandTotal > 0 && (