Skip to content

Commit

Permalink
[7.x] [Security Solution][Resolver] - Maintain active node (#86682) (#…
Browse files Browse the repository at this point in the history
…87371)

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
michaelolo24 and kibanamachine authored Jan 6, 2021
1 parent 63de344 commit 00bf236
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 81 deletions.
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 @@ -6,32 +6,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 @@ -51,8 +25,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 @@ -63,10 +45,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 @@ -111,7 +99,6 @@ export type ResolverAction =
| CameraAction
| DataAction
| AppReceivedNewExternalProperties
| UserBroughtNodeIntoView
| UserFocusedOnResolverNode
| UserSelectedResolverNode
| UserRequestedRelatedEventData;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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 @@ -59,7 +60,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 @@ -101,7 +102,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 @@ -28,3 +28,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 @@ -34,7 +34,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 @@ -92,26 +91,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 @@ -11,6 +11,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 @@ -30,22 +31,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 @@ -65,16 +58,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 @@ -151,7 +151,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 @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

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 @@ -15,6 +15,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 @@ -155,6 +156,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 @@ -288,9 +290,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 @@ -306,12 +311,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 @@ -24,6 +24,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 @@ -56,6 +57,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 @@ -149,7 +149,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 @@ -26,6 +26,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 @@ -294,7 +295,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 @@ -304,7 +305,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

0 comments on commit 00bf236

Please sign in to comment.