mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
8903d3427e
commit
893525c74c
7 changed files with 118 additions and 90 deletions
|
@ -9,7 +9,6 @@ import { DataState } from '../../types';
|
|||
import { ResolverAction } from '../actions';
|
||||
|
||||
const initialState: DataState = {
|
||||
relatedEventsStats: new Map(),
|
||||
relatedEvents: new Map(),
|
||||
relatedEventsReady: new Map(),
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>;
|
||||
/**
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue