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] Replace Selectable popover with badges #76997

Merged
merged 13 commits into from
Sep 10, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ type ResolverColorNames =
| 'resolverBackground'
| 'resolverEdge'
| 'resolverEdgeText'
| 'resolverBreadcrumbBackground';
| 'resolverBreadcrumbBackground'
| 'pillStroke';

type ColorMap = Record<ResolverColorNames, string>;
interface NodeStyleConfig {
Expand Down Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const StyledActionsContainer = styled.div<StyledActionsContainer>`
position: absolute;
top: ${(props) => `${props.topPct}%`};
width: auto;
pointer-events: all;
`;

interface StyledDescriptionText {
Expand Down Expand Up @@ -329,6 +330,7 @@ const UnstyledProcessEventDot = React.memo(
}
role="img"
aria-labelledby={labelHTMLID}
fill="none"
style={{
display: 'block',
width: '100%',
Expand All @@ -338,9 +340,10 @@ const UnstyledProcessEventDot = React.memo(
left: '0',
outline: 'transparent',
border: 'none',
pointerEvents: 'none',
}}
>
<g>
<g fill="none" style={{ pointerEvents: 'visiblePainted' }}>
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
<use
xlinkHref={`#${SymbolIds.processCubeActiveBacking}`}
fill={backingFill} // Only visible on hover
Expand Down Expand Up @@ -464,6 +467,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.
Expand Down
258 changes: 131 additions & 127 deletions x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -43,7 +34,7 @@ export const subMenuAssets = {
}),
},
};
const idGenerator = htmlIdGenerator();

interface ResolverSubmenuOption {
optionTitle: string;
action: () => unknown;
Expand All @@ -52,73 +43,46 @@ interface ResolverSubmenuOption {

export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string;

const OptionListItem = styled.div`
width: 175px;
`;

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 DetailHostButton = React.memo(
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
({
subMenuOptions,
isLoading,
hasMenu,
menuIsOpen,
action,
count,
title,
buttonBorderColor,
nodeID,
}: {
subMenuOptions: ResolverSubmenuOptionList;
isLoading: boolean;
hasMenu: boolean;
menuIsOpen?: boolean;
action: (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
count?: number;
title: string;
buttonBorderColor: ButtonColor;
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 hasIcon = hasMenu ?? false;
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
const iconType = menuIsOpen === true ? 'arrowUp' : 'arrowDown';
return (
<EuiSelectable
singleSelection={true}
options={options}
{...selectableProps}
isLoading={isLoading}
<EuiButton
onClick={action}
iconType={hasIcon ? iconType : 'none'}
fill={false}
color={'primary'}
style={{ height: 'fit-content', lineHeight: 1, padding: '.25em', fontSize: '.85rem' }}
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
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}
</EuiButton>
);
}
);
Expand Down Expand Up @@ -177,11 +141,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>();

Expand All @@ -204,6 +163,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`
Expand All @@ -222,44 +191,49 @@ 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>
<>
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
<DetailHostButton
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So much cleaner 😄

hasMenu={true}
menuIsOpen={menuIsOpen}
action={handleMenuOpenClick}
count={count}
title={menuTitle}
buttonBorderColor={buttonBorderColor}
nodeID={nodeID}
/>
{menuIsOpen ? (
<ul
className={`${className} options`}
aria-hidden={!menuIsOpen}
aria-owns={nodeID}
aria-describedby={nodeID}
>
{optionsWithActions
.sort((opta, optb) => {
return opta.optionTitle < optb.optionTitle ? -1 : 1;
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
})
.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}
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
</button>
</li>
);
})}
</ul>
) : null}
</>
);
}
);
Expand All @@ -271,6 +245,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};
Expand All @@ -283,16 +299,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;
}
`;