[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:
parent
07891be8fd
commit
060f0895cc
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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 });
|
||||
|
|
Loading…
Reference in a new issue