[Security Solution][Resolver] Replace Selectable popover with badges (#76997)

* replace selectable/popover with badges

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Brent Kimmel 2020-09-10 17:40:38 -04:00 committed by GitHub
parent e3c71a7b66
commit fcead4ffde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 146 additions and 128 deletions

View file

@ -24,7 +24,8 @@ type ResolverColorNames =
| 'resolverBackground'
| 'resolverEdge'
| 'resolverEdgeText'
| 'resolverBreadcrumbBackground';
| 'resolverBreadcrumbBackground'
| 'pillStroke';
type ColorMap = Record<ResolverColorNames, string>;
interface NodeStyleConfig {
@ -438,6 +439,7 @@ export const useResolverTheme = (): {
resolverBreadcrumbBackground: theme.euiColorLightestShade,
resolverEdgeText: getThemedOption(theme.euiColorDarkShade, theme.euiColorFullShade),
triggerBackingFill: `${theme.euiColorDanger}${getThemedOption('0F', '1F')}`,
pillStroke: theme.euiColorLightShade,
};
const nodeAssets: NodeStyleMap = {

View file

@ -38,6 +38,7 @@ const StyledActionsContainer = styled.div<StyledActionsContainer>`
position: absolute;
top: ${(props) => `${props.topPct}%`};
width: auto;
pointer-events: all;
`;
interface StyledDescriptionText {
@ -61,6 +62,11 @@ const StyledDescriptionText = styled.div<StyledDescriptionText>`
width: fit-content;
`;
const StyledOuterGroup = styled.g`
fill: none;
pointer-events: visiblePainted;
`;
/**
* An artifact that represents a process node and the things associated with it in the Resolver
*/
@ -329,6 +335,7 @@ const UnstyledProcessEventDot = React.memo(
}
role="img"
aria-labelledby={labelHTMLID}
fill="none"
style={{
display: 'block',
width: '100%',
@ -338,9 +345,10 @@ const UnstyledProcessEventDot = React.memo(
left: '0',
outline: 'transparent',
border: 'none',
pointerEvents: 'none',
}}
>
<g>
<StyledOuterGroup>
<use
xlinkHref={`#${SymbolIds.processCubeActiveBacking}`}
fill={backingFill} // Only visible on hover
@ -372,7 +380,7 @@ const UnstyledProcessEventDot = React.memo(
ref={animationTarget}
/>
</use>
</g>
</StyledOuterGroup>
</svg>
<StyledActionsContainer
color={colorMap.full}
@ -464,6 +472,7 @@ export const ProcessEventDot = styled(UnstyledProcessEventDot)`
min-width: 280px;
min-height: 90px;
overflow-y: visible;
pointer-events: none;
//dasharray & dashoffset should be equal to "pull" the stroke back
//when it is transitioned.

View file

@ -4,23 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable no-duplicate-imports */
/* eslint-disable react/display-name */
import { i18n } from '@kbn/i18n';
import React, { useState, useMemo, useCallback, useRef, useLayoutEffect } from 'react';
import {
EuiI18nNumber,
EuiSelectable,
EuiButton,
EuiPopover,
ButtonColor,
htmlIdGenerator,
} from '@elastic/eui';
import React, { useState, useCallback, useRef, useLayoutEffect, useMemo } from 'react';
import { EuiI18nNumber, EuiButton, EuiPopover, ButtonColor } from '@elastic/eui';
import styled from 'styled-components';
import { EuiSelectableOption } from '@elastic/eui';
import { Matrix3 } from '../types';
import { useResolverTheme } from './assets';
/**
* i18n-translated titles for submenus and identifiers for display of states:
@ -43,7 +34,7 @@ export const subMenuAssets = {
}),
},
};
const idGenerator = htmlIdGenerator();
interface ResolverSubmenuOption {
optionTitle: string;
action: () => unknown;
@ -52,73 +43,51 @@ interface ResolverSubmenuOption {
export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string;
const OptionListItem = styled.div`
width: 175px;
const StyledActionButton = styled(EuiButton)`
&.euiButton--small {
height: fit-content;
line-height: 1;
padding: 0.25em;
font-size: 0.85rem;
}
`;
const OptionList = React.memo(
/**
* 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(
({
subMenuOptions,
isLoading,
hasMenu,
menuIsOpen,
action,
count,
title,
nodeID,
}: {
subMenuOptions: ResolverSubmenuOptionList;
isLoading: boolean;
hasMenu: boolean;
menuIsOpen?: boolean;
action: (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
count?: number;
title: string;
nodeID: string;
}) => {
const [options, setOptions] = useState<EuiSelectableOption[]>(() =>
typeof subMenuOptions !== 'object'
? []
: subMenuOptions.map((option: ResolverSubmenuOption) => {
const dataTestSubj = 'resolver:map:node-submenu-item';
return option.prefix
? {
label: option.optionTitle,
prepend: <span>{option.prefix} </span>,
'data-test-subj': dataTestSubj,
}
: {
label: option.optionTitle,
prepend: <span />,
'data-test-subj': dataTestSubj,
};
})
);
const actionsByLabel: Record<string, () => unknown> = useMemo(() => {
if (typeof subMenuOptions !== 'object') {
return {};
}
return subMenuOptions.reduce((titleActionRecord, opt) => {
const { optionTitle, action } = opt;
return { ...titleActionRecord, [optionTitle]: action };
}, {});
}, [subMenuOptions]);
const selectableProps = useMemo(() => {
return {
listProps: { showIcons: true, bordered: true },
onChange: (newOptions: EuiSelectableOption[]) => {
const selectedOption = newOptions.find((opt) => opt.checked === 'on');
if (selectedOption) {
const { label } = selectedOption;
const actionToTake = actionsByLabel[label];
if (typeof actionToTake === 'function') {
actionToTake();
}
}
setOptions(newOptions);
},
};
}, [actionsByLabel]);
const iconType = menuIsOpen === true ? 'arrowUp' : 'arrowDown';
return (
<EuiSelectable
singleSelection={true}
options={options}
{...selectableProps}
isLoading={isLoading}
<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}
>
{(list) => <OptionListItem>{list}</OptionListItem>}
</EuiSelectable>
{count ? <EuiI18nNumber value={count} /> : ''} {title}
</StyledActionButton>
);
}
);
@ -177,11 +146,6 @@ const NodeSubMenuComponents = React.memo(
[menuAction]
);
const closePopover = useCallback(() => setMenuOpen(false), []);
const popoverId = idGenerator('submenu-popover');
const isMenuLoading = optionsWithActions === 'waitingForRelatedEventData';
// The last projection matrix that was used to position the popover
const projectionMatrixAtLastRender = useRef<Matrix3>();
@ -204,6 +168,16 @@ const NodeSubMenuComponents = React.memo(
projectionMatrixAtLastRender.current = projectionMatrix;
}, [projectionMatrixAtLastRender, projectionMatrix]);
const {
colorMap: { pillStroke: pillBorderStroke, resolverBackground: pillFill },
} = useResolverTheme();
const listStylesFromTheme = useMemo(() => {
return {
border: `1.5px solid ${pillBorderStroke}`,
backgroundColor: pillFill,
};
}, [pillBorderStroke, pillFill]);
if (!optionsWithActions) {
/**
* When called with a `menuAction`
@ -222,44 +196,47 @@ const NodeSubMenuComponents = React.memo(
</div>
);
}
/**
* When called with a set of `optionsWithActions`:
* Render with a panel of options that appear when the menu host button is clicked
*/
const submenuPopoverButton = (
<EuiButton
onClick={
typeof optionsWithActions === 'object' ? handleMenuOpenClick : handleMenuActionClick
}
color={buttonBorderColor}
size="s"
iconType={menuIsOpen ? 'arrowUp' : 'arrowDown'}
iconSide="right"
tabIndex={-1}
data-test-subj="resolver:submenu:button"
data-test-resolver-node-id={nodeID}
>
{count ? <EuiI18nNumber value={count} /> : ''} {menuTitle}
</EuiButton>
);
if (typeof optionsWithActions === 'string') {
return <></>;
}
return (
<div className={className + (menuIsOpen ? ' is-open' : '')}>
<EuiPopover
id={popoverId}
panelPaddingSize="none"
button={submenuPopoverButton}
isOpen={menuIsOpen}
closePopover={closePopover}
repositionOnScroll
ref={popoverRef}
>
{menuIsOpen && typeof optionsWithActions === 'object' && (
<OptionList isLoading={isMenuLoading} subMenuOptions={optionsWithActions} />
)}
</EuiPopover>
</div>
<>
<SubButton
hasMenu={true}
menuIsOpen={menuIsOpen}
action={handleMenuOpenClick}
count={count}
title={menuTitle}
nodeID={nodeID}
/>
{menuIsOpen ? (
<ul
className={`${className} options`}
aria-hidden={!menuIsOpen}
aria-describedby={nodeID}
>
{optionsWithActions
.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}
</>
);
}
);
@ -271,6 +248,48 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)`
display: flex;
flex-flow: column;
&.options {
font-size: 0.8rem;
display: flex;
flex-flow: row wrap;
background: transparent;
position: absolute;
top: 6.5em;
contain: content;
width: 12em;
z-index: 2;
}
&.options .item {
margin: 0.25ch 0.35ch 0.35ch 0;
padding: 0.35em 0.5em;
height: fit-content;
width: fit-content;
border-radius: 2px;
line-height: 0.8;
}
&.options .item button {
appearance: none;
height: fit-content;
width: fit-content;
line-height: 0.8;
outline-style: none;
border-color: transparent;
box-shadow: none;
}
&.options .item button:focus {
outline-style: none;
border-color: transparent;
box-shadow: none;
text-decoration: underline;
}
&.options .item button:active {
transform: scale(0.95);
}
& .euiButton {
background-color: ${(props) => props.buttonFill};
border-color: ${(props) => props.buttonBorderColor};
@ -283,16 +302,4 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)`
background-color: ${(props) => props.buttonFill};
}
}
& .euiPopover__anchor {
display: flex;
}
&.is-open .euiButton {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
&.is-open .euiSelectableListItem__prepend {
color: white;
}
`;