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

[Security Solution][Resolver] Show all event counts on nodes #77819

Merged
merged 13 commits into from
Sep 18, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,6 @@ const UnstyledProcessEventDot = React.memo(
});
}, [dispatch, nodeID]);

const handleRelatedEventRequest = useCallback(() => {
dispatch({
type: 'userRequestedRelatedEventData',
payload: nodeID,
});
}, [dispatch, nodeID]);

const handleClick = useCallback(
(clickEvent) => {
if (animationTarget.current?.beginElement) {
Expand Down Expand Up @@ -434,11 +427,7 @@ const UnstyledProcessEventDot = React.memo(
<EuiFlexItem grow={false} className="related-dropdown">
{grandTotal !== null && grandTotal > 0 && (
<NodeSubMenu
count={grandTotal}
buttonBorderColor={labelButtonFill}
buttonFill={colorMap.resolverBackground}
menuAction={handleRelatedEventRequest}
projectionMatrix={projectionMatrix}
relatedEventStats={relatedEventStats}
nodeID={nodeID}
/>
Expand Down
191 changes: 22 additions & 169 deletions x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/

/* eslint-disable react/display-name */

import { i18n } from '@kbn/i18n';
import React, { useState, useCallback, useRef, useLayoutEffect, useMemo } from 'react';
import { EuiI18nNumber, EuiButton, EuiPopover, ButtonColor } from '@elastic/eui';
import React, { useMemo } from 'react';
import { EuiI18nNumber } from '@elastic/eui';
import styled from 'styled-components';
import { ResolverNodeStats } from '../../../common/endpoint/types';
import { useRelatedEventByCategoryNavigation } from './use_related_event_by_category_navigation';
import { Matrix3 } from '../types';
import { useResolverTheme } from './assets';

/**
Expand Down Expand Up @@ -45,107 +42,27 @@ interface ResolverSubmenuOption {

export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string;

const StyledActionButton = styled(EuiButton)`
&.euiButton--small {
height: fit-content;
line-height: 1;
padding: 0.25em;
font-size: 0.85rem;
}
`;

/**
* This will be the "host button" that displays the "total number of related events" and opens
* the sumbmenu (with counts by category) when clicked.
*/
const SubButton = React.memo(
({
hasMenu,
menuIsOpen,
action,
count,
nodeID,
}: {
hasMenu: boolean;
menuIsOpen?: boolean;
action: (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
count?: number;
nodeID: string;
}) => {
const iconType = menuIsOpen === true ? 'arrowUp' : 'arrowDown';
return (
<StyledActionButton
onClick={action}
iconType={hasMenu ? iconType : 'none'}
fill={false}
color={'primary'}
size="s"
iconSide="right"
tabIndex={-1}
data-test-subj="resolver:submenu:button"
data-test-resolver-node-id={nodeID}
id={nodeID}
>
{count ? <EuiI18nNumber value={count} /> : ''} {subMenuAssets.relatedEvents.title}
</StyledActionButton>
);
}
);

/**
* A Submenu to be displayed in one of two forms:
* 1) Provided a collection of `optionsWithActions`: it will call `menuAction` then - if and when menuData becomes available - display each item with an optional prefix and call the supplied action for the options when that option is clicked.
* 2) Provided `optionsWithActions` is undefined, it will call the supplied `menuAction` when its host button is clicked.
*/
const NodeSubMenuComponents = React.memo(
({
count,
buttonBorderColor,
menuAction,
className,
projectionMatrix,
nodeID,
relatedEventStats,
}: {
className?: string;
menuAction?: () => unknown;
buttonBorderColor: ButtonColor;
// eslint-disable-next-line react/no-unused-prop-types
buttonFill: string;
count?: number;
/**
* Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself.
*/
projectionMatrix: Matrix3;
nodeID: string;
relatedEventStats: ResolverNodeStats | undefined;
}) => {
// keep a ref to the popover so we can call its reposition method
const popoverRef = useRef<EuiPopover>(null);

const [menuIsOpen, setMenuOpen] = useState(false);
const handleMenuOpenClick = useCallback(
(clickEvent: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
// stopping propagation/default to prevent other node animations from triggering
clickEvent.preventDefault();
clickEvent.stopPropagation();
setMenuOpen(!menuIsOpen);
},
[menuIsOpen]
);
const handleMenuActionClick = useCallback(
(clickEvent: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
// stopping propagation/default to prevent other node animations from triggering
clickEvent.preventDefault();
clickEvent.stopPropagation();
if (typeof menuAction === 'function') menuAction();
setMenuOpen(true);
},
[menuAction]
);

// The last projection matrix that was used to position the popover
const projectionMatrixAtLastRender = useRef<Matrix3>();
const relatedEventCallbacks = useRelatedEventByCategoryNavigation({
nodeID,
categories: relatedEventStats?.events?.byCategory,
Expand All @@ -164,24 +81,6 @@ const NodeSubMenuComponents = React.memo(
}
}, [relatedEventStats, relatedEventCallbacks]);

useLayoutEffect(() => {
if (
/**
* If there is a popover component reference,
* and this isn't the first render,
* and the projectionMatrix has changed since last render,
* then force the popover to reposition itself.
*/
popoverRef.current &&
projectionMatrixAtLastRender.current &&
projectionMatrixAtLastRender.current !== projectionMatrix
) {
popoverRef.current.positionPopoverFixed();
}

// no matter what, keep track of the last project matrix that was used to size the popover
projectionMatrixAtLastRender.current = projectionMatrix;
}, [projectionMatrixAtLastRender, projectionMatrix]);
const {
colorMap: { pillStroke: pillBorderStroke, resolverBackground: pillFill },
} = useResolverTheme();
Expand All @@ -191,64 +90,31 @@ const NodeSubMenuComponents = React.memo(
backgroundColor: pillFill,
};
}, [pillBorderStroke, pillFill]);
if (relatedEventStats === undefined) {
/**
* When called with a `menuAction`
* Render without dropdown and call the supplied action when host button is clicked
*/
return (
<div className={className}>
<EuiButton
onClick={handleMenuActionClick}
color={buttonBorderColor}
size="s"
tabIndex={-1}
>
{subMenuAssets.relatedEvents.title}
</EuiButton>
</div>
);
}

if (relatedEventOptions === undefined) {
return null;
}

return (
<>
<SubButton
hasMenu={true}
menuIsOpen={menuIsOpen}
action={handleMenuOpenClick}
count={count}
nodeID={nodeID}
/>
{menuIsOpen ? (
<ul
className={`${className} options`}
aria-hidden={!menuIsOpen}
aria-describedby={nodeID}
>
{relatedEventOptions
.sort((opta, optb) => {
return opta.optionTitle.localeCompare(optb.optionTitle);
})
.map((opt) => {
return (
<li
className="item"
data-test-subj="resolver:map:node-submenu-item"
style={listStylesFromTheme}
>
<button type="button" className="kbn-resetFocusState" onClick={opt.action}>
{opt.prefix} {opt.optionTitle}
</button>
</li>
);
})}
</ul>
) : null}
</>
<ul className={`${className} options`} aria-describedby={nodeID}>
{relatedEventOptions
.sort((opta, optb) => {
return opta.optionTitle.localeCompare(optb.optionTitle);
})
.map((opt) => {
return (
<li
className="item"
data-test-subj="resolver:map:node-submenu-item"
style={listStylesFromTheme}
>
<button type="button" className="kbn-resetFocusState" onClick={opt.action}>
{opt.prefix} {opt.optionTitle}
</button>
</li>
);
})}
</ul>
);
}
);
Expand All @@ -266,7 +132,7 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)`
flex-flow: row wrap;
background: transparent;
position: absolute;
top: 6.5em;
top: 4.5em;
contain: content;
width: 12em;
z-index: 2;
Expand Down Expand Up @@ -301,17 +167,4 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)`
&.options .item button:active {
transform: scale(0.95);
}

& .euiButton {
background-color: ${(props) => props.buttonFill};
border-color: ${(props) => props.buttonBorderColor};
border-style: solid;
border-width: 1px;

&:hover,
&:active,
&:focus {
background-color: ${(props) => props.buttonFill};
}
}
`;