Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.11] [Security Solution][Resolver] - Maintain active node (#86682) #91147

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 20 additions & 33 deletions x-pack/plugins/security_solution/public/resolver/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,6 @@
import { CameraAction } from './camera';
import { DataAction } from './data/action';

/**
* When the user wants to bring a node front-and-center on the map.
* @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties`
*/
interface UserBroughtNodeIntoView {
/**
* @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties`
*/
readonly type: 'userBroughtNodeIntoView';
/**
* @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties`
*/
readonly payload: {
/**
* Used to identify the node that should be brought into view.
* @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties`
*/
readonly nodeID: string;
/**
* The time (since epoch in milliseconds) when the action was dispatched.
* @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties`
*/
readonly time: number;
};
}

/**
* The action dispatched when the app requests related event data for one
* subject (whose entity_id should be included as `payload`)
Expand All @@ -53,8 +27,16 @@ interface UserRequestedRelatedEventData {
interface UserFocusedOnResolverNode {
readonly type: 'userFocusedOnResolverNode';

/** focused nodeID */
readonly payload: string;
readonly payload: {
/**
* Used to identify the node that should be brought into view.
*/
readonly nodeID: string;
/**
* The time (since epoch in milliseconds) when the action was dispatched.
*/
readonly time: number;
};
}

/**
Expand All @@ -65,10 +47,16 @@ interface UserFocusedOnResolverNode {
*/
interface UserSelectedResolverNode {
readonly type: 'userSelectedResolverNode';
/**
* The nodeID (aka entity_id) that was select.
*/
readonly payload: string;
readonly payload: {
/**
* Used to identify the node that should be brought into view.
*/
readonly nodeID: string;
/**
* The time (since epoch in milliseconds) when the action was dispatched.
*/
readonly time: number;
};
}

/**
Expand Down Expand Up @@ -113,7 +101,6 @@ export type ResolverAction =
| CameraAction
| DataAction
| AppReceivedNewExternalProperties
| UserBroughtNodeIntoView
| UserFocusedOnResolverNode
| UserSelectedResolverNode
| UserRequestedRelatedEventData;
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as selectors from './selectors';
import { animatePanning } from './methods';
import { lerp } from '../../lib/math';
import { ResolverAction } from '../actions';
import { panAnimationDuration } from './scaling_constants';

type TestAction =
| ResolverAction
Expand Down Expand Up @@ -60,7 +61,7 @@ describe('when the camera is created', () => {
});

describe('When attempting to pan to current position and scale', () => {
const duration = 1000;
const duration = panAnimationDuration;
const startTime = 0;
beforeEach(() => {
const action: TestAction = {
Expand Down Expand Up @@ -102,7 +103,7 @@ describe('when the camera is created', () => {
});

describe('when animation begins', () => {
const duration = 1000;
const duration = panAnimationDuration;
let targetTranslation: Vector2;
const startTime = 0;
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ export const unitsPerNudge = 50;
* The duration a nudge animation lasts.
*/
export const nudgeAnimationDuration = 300;

/**
* The duration a panning animation lasts
*/
export const panAnimationDuration = 1000;
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export function ResolverTreeFetcher(
return async () => {
const state = api.getState();
const databaseParameters = selectors.treeParametersToFetch(state);
const currentPanelParameters = selectors.panelViewAndParameters(state);

if (selectors.treeRequestParametersToAbort(state) && lastRequestAbortController) {
lastRequestAbortController.abort();
Expand Down Expand Up @@ -93,26 +92,6 @@ export function ResolverTreeFetcher(
parameters: databaseParameters,
},
});

/*
* Necessary to handle refresh states where another node besides the origin was selected
* If the user has selected another node, but is back to viewing the nodeList, nodeID won't be set in the url
* So after a refresh the focused node will be the originID.
* This is okay for now, but can be updated if we decide to track selectedNode in panelParameters.
*/
// no nodeID on the 'nodes' (nodeList) view.
if (currentPanelParameters && currentPanelParameters.panelView !== 'nodes') {
const { nodeID } = currentPanelParameters.panelParameters;
const urlHasDefinedNode = result.find((node) => node.id === nodeID);
api.dispatch({
type: 'userBroughtNodeIntoView',
payload: {
// In the event the origin is the url selectedNode, the animation has logic to prevent an unnecessary transition taking place
nodeID: urlHasDefinedNode ? nodeID : entityIDToFetch,
time: Date.now(),
},
});
}
} catch (error) {
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-AbortError
if (error instanceof DOMException && error.name === 'AbortError') {
Expand Down
26 changes: 11 additions & 15 deletions x-pack/plugins/security_solution/public/resolver/store/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { cameraReducer } from './camera/reducer';
import { dataReducer } from './data/reducer';
import { ResolverAction } from './actions';
import { ResolverState, ResolverUIState } from '../types';
import { panAnimationDuration } from './camera/scaling_constants';
import { nodePosition } from '../models/indexed_process_tree/isometric_taxi_layout';

const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
Expand All @@ -31,22 +32,14 @@ const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
} else if (action.type === 'userFocusedOnResolverNode') {
const next: ResolverUIState = {
...state,
ariaActiveDescendant: action.payload,
ariaActiveDescendant: action.payload.nodeID,
};
return next;
} else if (action.type === 'userSelectedResolverNode') {
const next: ResolverUIState = {
...state,
selectedNode: action.payload,
};
return next;
} else if (action.type === 'userBroughtNodeIntoView') {
const { nodeID } = action.payload;
const next: ResolverUIState = {
...state,
// Select the node. NB: Animation is handled in the reducer as well.
ariaActiveDescendant: nodeID,
selectedNode: nodeID,
selectedNode: action.payload.nodeID,
ariaActiveDescendant: action.payload.nodeID,
};
return next;
} else if (action.type === 'appReceivedNewExternalProperties') {
Expand All @@ -66,16 +59,19 @@ const concernReducers = combineReducers({
data: dataReducer,
ui: uiReducer,
});
const animationDuration = 1000;

export const resolverReducer: Reducer<ResolverState, ResolverAction> = (state, action) => {
const nextState = concernReducers(state, action);
if (action.type === 'userBroughtNodeIntoView') {
if (action.type === 'userSelectedResolverNode' || action.type === 'userFocusedOnResolverNode') {
const position = nodePosition(layout(nextState), action.payload.nodeID);
if (position) {
const withAnimation: ResolverState = {
...nextState,
camera: animatePanning(nextState.camera, action.payload.time, position, animationDuration),
camera: animatePanning(
nextState.camera,
action.payload.time,
position,
panAnimationDuration
),
};
return withAnimation;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function NodeDetailLink({ name, nodeID }: { name?: string; nodeID: string }) {
(mouseEvent: React.MouseEvent<HTMLAnchorElement>) => {
linkProps.onClick(mouseEvent);
dispatch({
type: 'userBroughtNodeIntoView',
type: 'userSelectedResolverNode',
payload: {
nodeID,
time: timestamp(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useContext } from 'react';
import styled from 'styled-components';
import { htmlIdGenerator, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useSelector } from 'react-redux';
Expand All @@ -16,6 +16,7 @@ import { applyMatrix3 } from '../models/vector2';
import { Vector2, Matrix3, ResolverState } from '../types';
import { ResolverNode } from '../../../common/endpoint/types';
import { useResolverDispatch } from './use_resolver_dispatch';
import { SideEffectContext } from './side_effect_context';
import * as nodeModel from '../../../common/endpoint/models/node';
import * as selectors from '../store/selectors';
import { fontSize } from './font_size';
Expand Down Expand Up @@ -156,6 +157,7 @@ const UnstyledProcessEventDot = React.memo(
const htmlIDPrefix = `resolver:${resolverComponentInstanceID}`;

const symbolIDs = useSymbolIDs();
const { timestamp } = useContext(SideEffectContext);

/**
* Convert the position, which is in 'world' coordinates, to screen coordinates.
Expand Down Expand Up @@ -289,9 +291,12 @@ const UnstyledProcessEventDot = React.memo(
const handleFocus = useCallback(() => {
dispatch({
type: 'userFocusedOnResolverNode',
payload: nodeID,
payload: {
nodeID,
time: timestamp(),
},
});
}, [dispatch, nodeID]);
}, [dispatch, nodeID, timestamp]);

const handleClick = useCallback(
(clickEvent) => {
Expand All @@ -307,12 +312,15 @@ const UnstyledProcessEventDot = React.memo(
} else {
dispatch({
type: 'userSelectedResolverNode',
payload: nodeID,
payload: {
nodeID,
time: timestamp(),
},
});
processDetailNavProps.onClick(clickEvent);
}
},
[animationTarget, dispatch, nodeID, processDetailNavProps, nodeState]
[animationTarget, dispatch, nodeID, processDetailNavProps, nodeState, timestamp]
);

const grandTotal: number | null = useSelector((state: ResolverState) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SideEffectContext } from './side_effect_context';
import { ResolverProps, ResolverState } from '../types';
import { PanelRouter } from './panels';
import { useColors } from './use_colors';
import { useSyncSelectedNode } from './use_sync_selected_node';

/**
* The highest level connected Resolver component. Needs a `Provider` in its ancestry to work.
Expand Down Expand Up @@ -57,6 +58,11 @@ export const ResolverWithoutProviders = React.memo(
filters,
});

/**
* This will keep the selectedNode in the view in sync with the nodeID specified in the url
*/
useSyncSelectedNode();

const { timestamp } = useContext(SideEffectContext);

// use this for the entire render in order to keep things in sync
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ const NodeSubmenuPill = ({
(mouseEvent: React.MouseEvent<HTMLButtonElement>) => {
linkProps.onClick(mouseEvent);
dispatch({
type: 'userBroughtNodeIntoView',
type: 'userSelectedResolverNode',
payload: {
nodeID,
time: timestamp(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as nodeModel from '../../../common/endpoint/models/node';
import { act } from 'react-dom/test-utils';
import { mockResolverNode } from '../mocks/resolver_node';
import { endpointSourceSchema } from '../mocks/tree_schema';
import { panAnimationDuration } from '../store/camera/scaling_constants';

describe('useCamera on an unpainted element', () => {
/** Enzyme full DOM wrapper for the element the camera is attached to. */
Expand Down Expand Up @@ -295,7 +296,7 @@ describe('useCamera on an unpainted element', () => {
throw new Error('could not find nodeID for process');
}
const cameraAction: ResolverAction = {
type: 'userBroughtNodeIntoView',
type: 'userSelectedResolverNode',
payload: {
time: simulator.controls.time,
nodeID,
Expand All @@ -305,7 +306,7 @@ describe('useCamera on an unpainted element', () => {
});

it('should request animation frames in a loop', () => {
const animationDuration = 1000;
const animationDuration = panAnimationDuration;
// When the animation begins, the camera should request an animation frame.
expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(1);

Expand Down
Loading