From fcead4ffde0a83efc1d6d2adb2d68a3b1fcc0cb3 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Thu, 10 Sep 2020 17:40:38 -0400 Subject: [PATCH] [Security Solution][Resolver] Replace Selectable popover with badges (#76997) * replace selectable/popover with badges Co-authored-by: Elastic Machine --- .../public/resolver/view/assets.tsx | 4 +- .../resolver/view/process_event_dot.tsx | 13 +- .../public/resolver/view/submenu.tsx | 257 +++++++++--------- 3 files changed, 146 insertions(+), 128 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index 6962d300f70726..1317c0ee94b601 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -24,7 +24,8 @@ type ResolverColorNames = | 'resolverBackground' | 'resolverEdge' | 'resolverEdgeText' - | 'resolverBreadcrumbBackground'; + | 'resolverBreadcrumbBackground' + | 'pillStroke'; type ColorMap = Record; 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 = { diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 2aacc5f9176c4d..5d7112dd1547af 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -38,6 +38,7 @@ const StyledActionsContainer = styled.div` position: absolute; top: ${(props) => `${props.topPct}%`}; width: auto; + pointer-events: all; `; interface StyledDescriptionText { @@ -61,6 +62,11 @@ const StyledDescriptionText = styled.div` 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', }} > - + - + 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) => void; + count?: number; + title: string; + nodeID: string; }) => { - const [options, setOptions] = useState(() => - typeof subMenuOptions !== 'object' - ? [] - : subMenuOptions.map((option: ResolverSubmenuOption) => { - const dataTestSubj = 'resolver:map:node-submenu-item'; - return option.prefix - ? { - label: option.optionTitle, - prepend: {option.prefix} , - 'data-test-subj': dataTestSubj, - } - : { - label: option.optionTitle, - prepend: , - 'data-test-subj': dataTestSubj, - }; - }) - ); - - const actionsByLabel: Record 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 ( - - {(list) => {list}} - + {count ? : ''} {title} + ); } ); @@ -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(); @@ -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( ); } - /** - * When called with a set of `optionsWithActions`: - * Render with a panel of options that appear when the menu host button is clicked - */ - const submenuPopoverButton = ( - - {count ? : ''} {menuTitle} - - ); + if (typeof optionsWithActions === 'string') { + return <>; + } return ( -
- - {menuIsOpen && typeof optionsWithActions === 'object' && ( - - )} - -
+ <> + + {menuIsOpen ? ( +
    + {optionsWithActions + .sort((opta, optb) => { + return opta.optionTitle.localeCompare(optb.optionTitle); + }) + .map((opt) => { + return ( +
  • + +
  • + ); + })} +
+ ) : 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; - } `;