mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
e3c71a7b66
commit
fcead4ffde
3 changed files with 146 additions and 128 deletions
|
@ -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 = {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue