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

* [Security Solution][Resolver] Show all event counts
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Brent Kimmel 2020-09-18 14:31:30 -04:00 committed by GitHub
parent 07891be8fd
commit 060f0895cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 28 additions and 223 deletions

View file

@ -276,48 +276,12 @@ describe('Resolver, when analyzing a tree that has two related events for the or
);
expect(edgesThatTerminateUnderneathSecondChild).toHaveLength(1);
});
it('should render a related events button', async () => {
it('should show exactly one option with the correct count', async () => {
await expect(
simulator.map(() => ({
relatedEventButtons: simulator.processNodeSubmenuButton(entityIDs.origin).length,
}))
).toYieldEqualTo({
relatedEventButtons: 1,
});
});
describe('when the related events button is clicked', () => {
beforeEach(async () => {
const button = await simulator.resolveWrapper(() =>
simulator.processNodeSubmenuButton(entityIDs.origin)
);
if (button) {
button.simulate('click', { button: 0 });
}
});
it('should open the submenu and display exactly one option with the correct count', async () => {
await expect(
simulator.map(() =>
simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text())
)
).toYieldEqualTo(['2 registry']);
});
});
describe('and when the related events button is clicked again', () => {
beforeEach(async () => {
const button = await simulator.resolveWrapper(() =>
simulator.processNodeSubmenuButton(entityIDs.origin)
);
if (button) {
button.simulate('click', { button: 0 });
button.simulate('click', { button: 0 }); // The first click opened the menu, this second click closes it
}
});
it('should close the submenu', async () => {
await expect(
simulator.map(() => simulator.testSubject('resolver:map:node-submenu-item').length)
).toYieldEqualTo(0);
});
simulator.map(() =>
simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text())
)
).toYieldEqualTo(['2 registry']);
});
});
});

View file

@ -257,13 +257,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) {
@ -439,11 +432,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}
/>

View file

@ -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 { useColors } from './use_colors';
/**
@ -45,53 +42,6 @@ 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.
@ -99,53 +49,20 @@ const SubButton = React.memo(
*/
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,
@ -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 { pillStroke: pillBorderStroke, resolverBackground: pillFill } = useColors();
const listStylesFromTheme = useMemo(() => {
return {
@ -189,65 +88,32 @@ 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}
key={opt.optionTitle}
>
<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}
key={opt.optionTitle}
>
<button type="button" className="kbn-resetFocusState" onClick={opt.action}>
{opt.prefix} {opt.optionTitle}
</button>
</li>
);
})}
</ul>
);
}
);
@ -265,7 +131,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;
@ -300,17 +166,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};
}
}
`;

View file

@ -13,7 +13,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const queryBar = getService('queryBar');
// FLAKY: https://github.com/elastic/kibana/issues/77835
describe('Endpoint Event Resolver', function () {
before(async () => {
await esArchiver.load('endpoint/resolver_tree', { useCreate: true });