mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Add Timeline Hover Action to Security Dashboard Calculated Metrics (#154299)
Brings back the Add to timeline action on alert counts via Cell Actions Areas implemented include: - D&R Dashboard - Open alerts by rule - Hosts by alert severity - Users by alert severity - Entity Analytics Dashboard - Host Risk Scores - User Risk Scores  --------- Crafted with friendship by Co-authored-by: Pablo Neves Machado <pablo.nevesmachado@elastic.co> Co-authored-by: semd <sergi.massaneda@elastic.co>
This commit is contained in:
parent
e1b3bc259a
commit
22b8e5b0a6
37 changed files with 885 additions and 196 deletions
|
@ -6,7 +6,7 @@ Example:
|
|||
```JSX
|
||||
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
|
||||
[...]
|
||||
<CellActions mode={CellActionsMode.HOVER} triggerId={MY_TRIGGER_ID} config={{ field: 'fieldName', value: 'fieldValue', fieldType: 'text' }}>
|
||||
<CellActions mode={CellActionsMode.HOVER_DOWN} triggerId={MY_TRIGGER_ID} config={{ field: 'fieldName', value: 'fieldValue', fieldType: 'text' }}>
|
||||
Hover me
|
||||
</CellActions>
|
||||
</CellActionsProvider>
|
||||
|
|
|
@ -50,8 +50,8 @@ export const DefaultWithControls = CellActionsTemplate.bind({});
|
|||
|
||||
DefaultWithControls.argTypes = {
|
||||
mode: {
|
||||
options: [CellActionsMode.HOVER, CellActionsMode.INLINE],
|
||||
defaultValue: CellActionsMode.HOVER,
|
||||
options: [CellActionsMode.HOVER_DOWN, CellActionsMode.INLINE],
|
||||
defaultValue: CellActionsMode.HOVER_DOWN,
|
||||
control: {
|
||||
type: 'radio',
|
||||
},
|
||||
|
@ -72,8 +72,14 @@ export const CellActionInline = ({}: {}) => (
|
|||
</CellActions>
|
||||
);
|
||||
|
||||
export const CellActionHoverPopup = ({}: {}) => (
|
||||
<CellActions mode={CellActionsMode.HOVER} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
export const CellActionHoverPopoverDown = ({}: {}) => (
|
||||
<CellActions mode={CellActionsMode.HOVER_DOWN} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Hover me
|
||||
</CellActions>
|
||||
);
|
||||
|
||||
export const CellActionHoverPopoverRight = ({}: {}) => (
|
||||
<CellActions mode={CellActionsMode.HOVER_RIGHT} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Hover me
|
||||
</CellActions>
|
||||
);
|
||||
|
|
|
@ -15,6 +15,11 @@ import { CellActionsProvider } from '../context/cell_actions_context';
|
|||
const TRIGGER_ID = 'test-trigger-id';
|
||||
const FIELD = { name: 'name', value: '123', type: 'text' };
|
||||
|
||||
jest.mock('./hover_actions_popover', () => ({
|
||||
HoverActionsPopover: jest.fn((props) => (
|
||||
<span data-test-subj="hoverActionsPopover">{props.anchorPosition}</span>
|
||||
)),
|
||||
}));
|
||||
describe('CellActions', () => {
|
||||
it('renders', async () => {
|
||||
const getActionsPromise = Promise.resolve([]);
|
||||
|
@ -54,13 +59,13 @@ describe('CellActions', () => {
|
|||
expect(queryByTestId('inlineActions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders HoverActionsPopover when mode is HOVER', async () => {
|
||||
it('renders HoverActionsPopover when mode is HOVER_DOWN', async () => {
|
||||
const getActionsPromise = Promise.resolve([]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { queryByTestId } = render(
|
||||
const { getByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<CellActions mode={CellActionsMode.HOVER} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
<CellActions mode={CellActionsMode.HOVER_DOWN} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Field value
|
||||
</CellActions>
|
||||
</CellActionsProvider>
|
||||
|
@ -70,6 +75,27 @@ describe('CellActions', () => {
|
|||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument();
|
||||
expect(getByTestId('hoverActionsPopover')).toBeInTheDocument();
|
||||
expect(getByTestId('hoverActionsPopover')).toHaveTextContent('downCenter');
|
||||
});
|
||||
|
||||
it('renders HoverActionsPopover when mode is HOVER_RIGHT', async () => {
|
||||
const getActionsPromise = Promise.resolve([]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { getByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<CellActions mode={CellActionsMode.HOVER_RIGHT} triggerId={TRIGGER_ID} field={FIELD}>
|
||||
Field value
|
||||
</CellActions>
|
||||
</CellActionsProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await getActionsPromise;
|
||||
});
|
||||
|
||||
expect(getByTestId('hoverActionsPopover')).toBeInTheDocument();
|
||||
expect(getByTestId('hoverActionsPopover')).toHaveTextContent('rightCenter');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,11 +36,17 @@ export const CellActions: React.FC<CellActionsProps> = ({
|
|||
[field, triggerId, metadata]
|
||||
);
|
||||
|
||||
const anchorPosition = useMemo(
|
||||
() => (mode === CellActionsMode.HOVER_DOWN ? 'downCenter' : 'rightCenter'),
|
||||
[mode]
|
||||
);
|
||||
|
||||
const dataTestSubj = `cellActions-renderContent-${field.name}`;
|
||||
if (mode === CellActionsMode.HOVER) {
|
||||
if (mode === CellActionsMode.HOVER_DOWN || mode === CellActionsMode.HOVER_RIGHT) {
|
||||
return (
|
||||
<div className={className} ref={nodeRef} data-test-subj={dataTestSubj}>
|
||||
<HoverActionsPopover
|
||||
anchorPosition={anchorPosition}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={showActionTooltips}
|
||||
visibleCellActions={visibleCellActions}
|
||||
|
@ -65,6 +71,7 @@ export const CellActions: React.FC<CellActionsProps> = ({
|
|||
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<InlineActions
|
||||
anchorPosition={anchorPosition}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={showActionTooltips}
|
||||
visibleCellActions={visibleCellActions}
|
||||
|
|
|
@ -12,17 +12,17 @@ import { makeAction, makeActionContext } from '../mocks/helpers';
|
|||
import { ExtraActionsPopOver, ExtraActionsPopOverWithAnchor } from './extra_actions_popover';
|
||||
|
||||
const actionContext = makeActionContext();
|
||||
const defaultProps = {
|
||||
anchorPosition: 'rightCenter' as const,
|
||||
actionContext,
|
||||
isOpen: false,
|
||||
closePopOver: () => {},
|
||||
actions: [],
|
||||
button: <span />,
|
||||
};
|
||||
describe('ExtraActionsPopOver', () => {
|
||||
it('renders', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ExtraActionsPopOver
|
||||
actionContext={actionContext}
|
||||
isOpen={false}
|
||||
closePopOver={() => {}}
|
||||
actions={[]}
|
||||
button={<span />}
|
||||
/>
|
||||
);
|
||||
const { queryByTestId } = render(<ExtraActionsPopOver {...defaultProps} />);
|
||||
|
||||
expect(queryByTestId('extraActionsPopOver')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -33,11 +33,10 @@ describe('ExtraActionsPopOver', () => {
|
|||
const action = { ...makeAction('test-action'), execute: executeAction };
|
||||
const { getByLabelText } = render(
|
||||
<ExtraActionsPopOver
|
||||
actionContext={actionContext}
|
||||
{...defaultProps}
|
||||
isOpen={true}
|
||||
closePopOver={closePopOver}
|
||||
actions={[action]}
|
||||
button={<span />}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -56,13 +55,7 @@ describe('ExtraActionsPopOverWithAnchor', () => {
|
|||
|
||||
it('renders', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ExtraActionsPopOverWithAnchor
|
||||
actionContext={actionContext}
|
||||
isOpen={false}
|
||||
closePopOver={() => {}}
|
||||
actions={[]}
|
||||
anchorRef={{ current: anchorElement }}
|
||||
/>
|
||||
<ExtraActionsPopOverWithAnchor {...defaultProps} anchorRef={{ current: anchorElement }} />
|
||||
);
|
||||
|
||||
expect(queryByTestId('extraActionsPopOverWithAnchor')).toBeInTheDocument();
|
||||
|
@ -74,7 +67,7 @@ describe('ExtraActionsPopOverWithAnchor', () => {
|
|||
const action = { ...makeAction('test-action'), execute: executeAction };
|
||||
const { getByLabelText } = render(
|
||||
<ExtraActionsPopOverWithAnchor
|
||||
actionContext={actionContext}
|
||||
{...defaultProps}
|
||||
isOpen={true}
|
||||
closePopOver={closePopOver}
|
||||
actions={[action]}
|
||||
|
|
|
@ -24,6 +24,7 @@ const euiContextMenuItemCSS = css`
|
|||
`;
|
||||
|
||||
interface ActionsPopOverProps {
|
||||
anchorPosition: 'rightCenter' | 'downCenter';
|
||||
actionContext: CellActionExecutionContext;
|
||||
isOpen: boolean;
|
||||
closePopOver: () => void;
|
||||
|
@ -32,6 +33,7 @@ interface ActionsPopOverProps {
|
|||
}
|
||||
|
||||
export const ExtraActionsPopOver: React.FC<ActionsPopOverProps> = ({
|
||||
anchorPosition,
|
||||
actions,
|
||||
actionContext,
|
||||
isOpen,
|
||||
|
@ -43,7 +45,7 @@ export const ExtraActionsPopOver: React.FC<ActionsPopOverProps> = ({
|
|||
isOpen={isOpen}
|
||||
closePopover={closePopOver}
|
||||
panelPaddingSize="xs"
|
||||
anchorPosition={'downCenter'}
|
||||
anchorPosition={anchorPosition}
|
||||
hasArrow
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
|
@ -59,11 +61,15 @@ export const ExtraActionsPopOver: React.FC<ActionsPopOverProps> = ({
|
|||
);
|
||||
|
||||
interface ExtraActionsPopOverWithAnchorProps
|
||||
extends Pick<ActionsPopOverProps, 'actionContext' | 'closePopOver' | 'isOpen' | 'actions'> {
|
||||
extends Pick<
|
||||
ActionsPopOverProps,
|
||||
'anchorPosition' | 'actionContext' | 'closePopOver' | 'isOpen' | 'actions'
|
||||
> {
|
||||
anchorRef: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const ExtraActionsPopOverWithAnchor = ({
|
||||
anchorPosition,
|
||||
anchorRef,
|
||||
actionContext,
|
||||
isOpen,
|
||||
|
@ -77,7 +83,7 @@ export const ExtraActionsPopOverWithAnchor = ({
|
|||
isOpen={isOpen}
|
||||
closePopover={closePopOver}
|
||||
panelPaddingSize="xs"
|
||||
anchorPosition={'downCenter'}
|
||||
anchorPosition={anchorPosition}
|
||||
hasArrow={false}
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
|
|
|
@ -13,11 +13,17 @@ import { makeAction } from '../mocks/helpers';
|
|||
import { CellActionExecutionContext } from '../types';
|
||||
import { HoverActionsPopover } from './hover_actions_popover';
|
||||
|
||||
describe('HoverActionsPopover', () => {
|
||||
const actionContext = {
|
||||
const defaultProps = {
|
||||
anchorPosition: 'rightCenter' as const,
|
||||
disabledActionTypes: [],
|
||||
visibleCellActions: 4,
|
||||
actionContext: {
|
||||
trigger: { id: 'triggerId' },
|
||||
field: { name: 'fieldName' },
|
||||
} as CellActionExecutionContext;
|
||||
} as CellActionExecutionContext,
|
||||
showActionTooltips: false,
|
||||
};
|
||||
describe('HoverActionsPopover', () => {
|
||||
const TestComponent = () => <span data-test-subj="test-component" />;
|
||||
jest.useFakeTimers();
|
||||
|
||||
|
@ -25,13 +31,7 @@ describe('HoverActionsPopover', () => {
|
|||
const getActions = () => Promise.resolve([]);
|
||||
const { queryByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
disabledActionTypes={[]}
|
||||
children={null}
|
||||
visibleCellActions={4}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
/>
|
||||
<HoverActionsPopover {...defaultProps} children={null} />
|
||||
</CellActionsProvider>
|
||||
);
|
||||
expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument();
|
||||
|
@ -44,12 +44,7 @@ describe('HoverActionsPopover', () => {
|
|||
|
||||
const { queryByLabelText, getByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
disabledActionTypes={[]}
|
||||
visibleCellActions={4}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<HoverActionsPopover {...defaultProps}>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsProvider>
|
||||
|
@ -70,12 +65,7 @@ describe('HoverActionsPopover', () => {
|
|||
|
||||
const { queryByLabelText, getByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
disabledActionTypes={[]}
|
||||
visibleCellActions={4}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<HoverActionsPopover {...defaultProps}>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsProvider>
|
||||
|
@ -101,12 +91,7 @@ describe('HoverActionsPopover', () => {
|
|||
|
||||
const { getByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
disabledActionTypes={[]}
|
||||
visibleCellActions={1}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<HoverActionsPopover {...defaultProps} visibleCellActions={1}>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsProvider>
|
||||
|
@ -127,12 +112,7 @@ describe('HoverActionsPopover', () => {
|
|||
|
||||
const { getByTestId, getByLabelText } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
disabledActionTypes={[]}
|
||||
visibleCellActions={1}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<HoverActionsPopover {...defaultProps} visibleCellActions={1}>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsProvider>
|
||||
|
@ -162,12 +142,7 @@ describe('HoverActionsPopover', () => {
|
|||
|
||||
const { getByTestId, queryByLabelText } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover
|
||||
disabledActionTypes={[]}
|
||||
visibleCellActions={2}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
>
|
||||
<HoverActionsPopover {...defaultProps} visibleCellActions={2}>
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsProvider>
|
||||
|
@ -191,6 +166,44 @@ describe('HoverActionsPopover', () => {
|
|||
expect(queryByLabelText('test-action-2')).toBeInTheDocument();
|
||||
expect(queryByLabelText('test-action-3')).toBeInTheDocument();
|
||||
});
|
||||
it('does not add css positioning when anchorPosition = downCenter', async () => {
|
||||
const getActionsPromise = Promise.resolve([makeAction('test-action')]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { getByLabelText, getByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover {...defaultProps} anchorPosition="downCenter">
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsProvider>
|
||||
);
|
||||
|
||||
await hoverElement(getByTestId('test-component'), async () => {
|
||||
await getActionsPromise;
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(Object.values(getByLabelText('Actions').style).includes('margin-top')).toEqual(false);
|
||||
});
|
||||
it('adds css positioning when anchorPosition = rightCenter', async () => {
|
||||
const getActionsPromise = Promise.resolve([makeAction('test-action')]);
|
||||
const getActions = () => getActionsPromise;
|
||||
|
||||
const { getByLabelText, getByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<HoverActionsPopover {...defaultProps} anchorPosition="rightCenter">
|
||||
<TestComponent />
|
||||
</HoverActionsPopover>
|
||||
</CellActionsProvider>
|
||||
);
|
||||
|
||||
await hoverElement(getByTestId('test-component'), async () => {
|
||||
await getActionsPromise;
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(Object.values(getByLabelText('Actions').style).includes('margin-top')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
const hoverElement = async (element: Element, waitForChange: () => Promise<unknown>) => {
|
||||
|
|
|
@ -36,6 +36,7 @@ const hoverContentWrapperCSS = css`
|
|||
const HOVER_INTENT_DELAY = 100; // ms
|
||||
|
||||
interface Props {
|
||||
anchorPosition: 'downCenter' | 'rightCenter';
|
||||
children: React.ReactNode;
|
||||
visibleCellActions: number;
|
||||
actionContext: CellActionExecutionContext;
|
||||
|
@ -44,6 +45,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const HoverActionsPopover: React.FC<Props> = ({
|
||||
anchorPosition,
|
||||
children,
|
||||
visibleCellActions,
|
||||
actionContext,
|
||||
|
@ -115,12 +117,17 @@ export const HoverActionsPopover: React.FC<Props> = ({
|
|||
);
|
||||
}, [onMouseEnter, closeExtraActions, children]);
|
||||
|
||||
const panelStyle = useMemo(
|
||||
() => (anchorPosition === 'rightCenter' ? { marginTop: euiThemeVars.euiSizeS } : {}),
|
||||
[anchorPosition]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onMouseLeave={closePopover}>
|
||||
<EuiPopover
|
||||
panelStyle={PANEL_STYLE}
|
||||
anchorPosition={'downCenter'}
|
||||
panelStyle={{ ...PANEL_STYLE, ...panelStyle }}
|
||||
anchorPosition={anchorPosition}
|
||||
button={content}
|
||||
closePopover={closePopover}
|
||||
hasArrow={false}
|
||||
|
@ -155,6 +162,7 @@ export const HoverActionsPopover: React.FC<Props> = ({
|
|||
</EuiPopover>
|
||||
</div>
|
||||
<ExtraActionsPopOverWithAnchor
|
||||
anchorPosition={anchorPosition}
|
||||
actions={extraActions}
|
||||
anchorRef={contentRef}
|
||||
actionContext={actionContext}
|
||||
|
|
|
@ -13,19 +13,20 @@ import { InlineActions } from './inline_actions';
|
|||
import { CellActionExecutionContext } from '../types';
|
||||
import { CellActionsProvider } from '../context';
|
||||
|
||||
const defaultProps = {
|
||||
anchorPosition: 'rightCenter' as const,
|
||||
disabledActionTypes: [],
|
||||
visibleCellActions: 5,
|
||||
actionContext: { trigger: { id: 'triggerId' } } as CellActionExecutionContext,
|
||||
showActionTooltips: false,
|
||||
};
|
||||
describe('InlineActions', () => {
|
||||
const actionContext = { trigger: { id: 'triggerId' } } as CellActionExecutionContext;
|
||||
it('renders', async () => {
|
||||
const getActionsPromise = Promise.resolve([]);
|
||||
const getActions = () => getActionsPromise;
|
||||
const { queryByTestId } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<InlineActions
|
||||
disabledActionTypes={[]}
|
||||
visibleCellActions={5}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
/>
|
||||
<InlineActions {...defaultProps} />
|
||||
</CellActionsProvider>
|
||||
);
|
||||
|
||||
|
@ -47,12 +48,7 @@ describe('InlineActions', () => {
|
|||
const getActions = () => getActionsPromise;
|
||||
const { queryAllByRole } = render(
|
||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||
<InlineActions
|
||||
disabledActionTypes={[]}
|
||||
visibleCellActions={5}
|
||||
actionContext={actionContext}
|
||||
showActionTooltips={false}
|
||||
/>
|
||||
<InlineActions {...defaultProps} />
|
||||
</CellActionsProvider>
|
||||
);
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useLoadActions } from '../hooks/use_load_actions';
|
|||
|
||||
interface InlineActionsProps {
|
||||
actionContext: CellActionExecutionContext;
|
||||
anchorPosition: 'rightCenter' | 'downCenter';
|
||||
showActionTooltips: boolean;
|
||||
visibleCellActions: number;
|
||||
disabledActionTypes: string[];
|
||||
|
@ -24,6 +25,7 @@ interface InlineActionsProps {
|
|||
|
||||
export const InlineActions: React.FC<InlineActionsProps> = ({
|
||||
actionContext,
|
||||
anchorPosition,
|
||||
showActionTooltips,
|
||||
visibleCellActions,
|
||||
disabledActionTypes,
|
||||
|
@ -47,10 +49,9 @@ export const InlineActions: React.FC<InlineActionsProps> = ({
|
|||
data-test-subj="inlineActions"
|
||||
className={`inlineActions ${isPopoverOpen ? 'inlineActions-popoverOpen' : ''}`}
|
||||
>
|
||||
{visibleActions.map((action, index) => (
|
||||
<EuiFlexItem grow={false}>
|
||||
{visibleActions.map((action) => (
|
||||
<EuiFlexItem grow={false} key={`action-item-${action.id}`}>
|
||||
<ActionItem
|
||||
key={`action-item-${index}`}
|
||||
action={action}
|
||||
actionContext={actionContext}
|
||||
showTooltip={showActionTooltips}
|
||||
|
@ -62,6 +63,7 @@ export const InlineActions: React.FC<InlineActionsProps> = ({
|
|||
<ExtraActionsPopOver
|
||||
actions={extraActions}
|
||||
actionContext={actionContext}
|
||||
anchorPosition={anchorPosition}
|
||||
button={button}
|
||||
closePopOver={closePopOver}
|
||||
isOpen={isPopoverOpen}
|
||||
|
|
|
@ -10,6 +10,7 @@ export const FILTER_CELL_ACTION_TYPE = 'cellAction-filter';
|
|||
export const COPY_CELL_ACTION_TYPE = 'cellAction-copy';
|
||||
|
||||
export enum CellActionsMode {
|
||||
HOVER = 'hover',
|
||||
HOVER_DOWN = 'hover-down',
|
||||
HOVER_RIGHT = 'hover-right',
|
||||
INLINE = 'inline',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SecurityAppStore } from '../../../common/store/types';
|
||||
import type { DataProvider } from '../../../../common/types';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import { addProvider } from '../../../timelines/store/timeline/actions';
|
||||
import { createAddToNewTimelineCellActionFactory, getToastMessage } from './add_to_new_timeline';
|
||||
import type { CellActionExecutionContext } from '@kbn/cell-actions';
|
||||
import { GEO_FIELD_TYPE } from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import { timelineActions } from '../../../timelines/store/timeline';
|
||||
|
||||
const services = createStartServicesMock();
|
||||
const mockWarningToast = services.notifications.toasts.addWarning;
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
const store = {
|
||||
dispatch: mockDispatch,
|
||||
} as unknown as SecurityAppStore;
|
||||
|
||||
const value = 'the-value';
|
||||
|
||||
const context = {
|
||||
field: { name: 'user.name', value, type: 'text' },
|
||||
} as CellActionExecutionContext;
|
||||
|
||||
const defaultAddProviderAction = {
|
||||
type: addProvider.type,
|
||||
payload: {
|
||||
id: TimelineId.active,
|
||||
providers: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: 'event-field-default-timeline-1-user_name-0-the-value',
|
||||
kqlQuery: '',
|
||||
name: 'user.name',
|
||||
queryMatch: {
|
||||
field: 'user.name',
|
||||
operator: ':',
|
||||
value: 'the-value',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('createAddToNewTimelineCellAction', () => {
|
||||
const addToTimelineCellActionFactory = createAddToNewTimelineCellActionFactory({
|
||||
store,
|
||||
services,
|
||||
});
|
||||
const addToTimelineAction = addToTimelineCellActionFactory({ id: 'testAddToTimeline', order: 1 });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return display name', () => {
|
||||
expect(addToTimelineAction.getDisplayName(context)).toEqual('Investigate in timeline');
|
||||
});
|
||||
|
||||
it('should return icon type', () => {
|
||||
expect(addToTimelineAction.getIconType(context)).toEqual('timeline');
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
it('should return true if everything is okay', async () => {
|
||||
expect(await addToTimelineAction.isCompatible(context)).toEqual(true);
|
||||
});
|
||||
it('should return false if field not allowed', async () => {
|
||||
expect(
|
||||
await addToTimelineAction.isCompatible({
|
||||
...context,
|
||||
field: { ...context.field, name: 'signal.reason' },
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute normally', async () => {
|
||||
await addToTimelineAction.execute(context);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(defaultAddProviderAction);
|
||||
expect(mockWarningToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show warning if no provider added', async () => {
|
||||
await addToTimelineAction.execute({
|
||||
...context,
|
||||
field: {
|
||||
...context.field,
|
||||
type: GEO_FIELD_TYPE,
|
||||
},
|
||||
});
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockWarningToast).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('should execute correctly when negateFilters is provided', () => {
|
||||
it('should not exclude if negateFilters is false', async () => {
|
||||
await addToTimelineAction.execute({
|
||||
...context,
|
||||
metadata: {
|
||||
negateFilters: false,
|
||||
},
|
||||
});
|
||||
expect(mockDispatch).toHaveBeenCalledWith(defaultAddProviderAction);
|
||||
expect(mockWarningToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should exclude if negateFilters is true', async () => {
|
||||
await addToTimelineAction.execute({
|
||||
...context,
|
||||
metadata: {
|
||||
negateFilters: true,
|
||||
},
|
||||
});
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
...defaultAddProviderAction,
|
||||
payload: {
|
||||
...defaultAddProviderAction.payload,
|
||||
providers: [{ ...defaultAddProviderAction.payload.providers[0], excluded: true }],
|
||||
},
|
||||
});
|
||||
expect(mockWarningToast).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear the timeline', async () => {
|
||||
await addToTimelineAction.execute(context);
|
||||
expect(mockDispatch.mock.calls[0][0].type).toEqual(timelineActions.createTimeline.type);
|
||||
});
|
||||
|
||||
it('should add the providers to the timeline', async () => {
|
||||
await addToTimelineAction.execute({
|
||||
...context,
|
||||
metadata: {
|
||||
andFilters: [{ field: 'kibana.alert.severity', value: 'low' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockDispatch).toBeCalledWith({
|
||||
...defaultAddProviderAction,
|
||||
payload: {
|
||||
...defaultAddProviderAction.payload,
|
||||
providers: [
|
||||
{
|
||||
...defaultAddProviderAction.payload.providers[0],
|
||||
id: 'event-field-default-timeline-1-user_name-0-the-value',
|
||||
queryMatch: defaultAddProviderAction.payload.providers[0].queryMatch,
|
||||
and: [
|
||||
{
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: 'event-field-default-timeline-1-kibana_alert_severity-0-low',
|
||||
kqlQuery: '',
|
||||
name: 'kibana.alert.severity',
|
||||
queryMatch: {
|
||||
field: 'kibana.alert.severity',
|
||||
operator: ':',
|
||||
value: 'low',
|
||||
},
|
||||
and: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToastMessage', () => {
|
||||
it('handles empty input', () => {
|
||||
const result = getToastMessage({ queryMatch: { value: null } } as unknown as DataProvider);
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
it('handles array input', () => {
|
||||
const result = getToastMessage({
|
||||
queryMatch: { value: ['hello', 'world'] },
|
||||
} as unknown as DataProvider);
|
||||
expect(result).toEqual('hello, world alerts');
|
||||
});
|
||||
|
||||
it('handles single filter', () => {
|
||||
const result = getToastMessage({
|
||||
queryMatch: { value },
|
||||
and: [{ queryMatch: { field: 'kibana.alert.severity', value: 'critical' } }],
|
||||
} as unknown as DataProvider);
|
||||
expect(result).toEqual(`critical severity alerts from ${value}`);
|
||||
});
|
||||
|
||||
it('handles multiple filters', () => {
|
||||
const result = getToastMessage({
|
||||
queryMatch: { value },
|
||||
and: [
|
||||
{
|
||||
queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
},
|
||||
{
|
||||
queryMatch: { field: 'kibana.alert.severity', value: 'critical' },
|
||||
},
|
||||
],
|
||||
} as unknown as DataProvider);
|
||||
expect(result).toEqual(`open, critical severity alerts from ${value}`);
|
||||
});
|
||||
|
||||
it('ignores unrelated filters', () => {
|
||||
const result = getToastMessage({
|
||||
queryMatch: { value },
|
||||
and: [
|
||||
{
|
||||
queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
},
|
||||
{
|
||||
queryMatch: { field: 'kibana.alert.severity', value: 'critical' },
|
||||
},
|
||||
// currently only supporting the above fields
|
||||
{
|
||||
queryMatch: { field: 'user.name', value: 'something' },
|
||||
},
|
||||
],
|
||||
} as unknown as DataProvider);
|
||||
expect(result).toEqual(`open, critical severity alerts from ${value}`);
|
||||
});
|
||||
|
||||
it('returns entity only when unrelated filters are passed', () => {
|
||||
const result = getToastMessage({
|
||||
queryMatch: { value },
|
||||
and: [{ queryMatch: { field: 'user.name', value: 'something' } }],
|
||||
} as unknown as DataProvider);
|
||||
expect(result).toEqual(`${value} alerts`);
|
||||
});
|
||||
|
||||
it('returns entity only when no filters are passed', () => {
|
||||
const result = getToastMessage({ queryMatch: { value }, and: [] } as unknown as DataProvider);
|
||||
expect(result).toEqual(`${value} alerts`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createCellActionFactory, type CellActionTemplate } from '@kbn/cell-actions';
|
||||
import { timelineActions } from '../../../timelines/store/timeline';
|
||||
import { addProvider } from '../../../timelines/store/timeline/actions';
|
||||
import type { DataProvider } from '../../../../common/types';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import type { SecurityAppStore } from '../../../common/store';
|
||||
import { fieldHasCellActions } from '../../utils';
|
||||
import {
|
||||
ADD_TO_NEW_TIMELINE,
|
||||
ADD_TO_TIMELINE_FAILED_TEXT,
|
||||
ADD_TO_TIMELINE_FAILED_TITLE,
|
||||
ADD_TO_TIMELINE_ICON,
|
||||
ADD_TO_TIMELINE_SUCCESS_TITLE,
|
||||
ALERTS_COUNT,
|
||||
SEVERITY,
|
||||
} from '../constants';
|
||||
import { createDataProviders, isValidDataProviderField } from '../data_provider';
|
||||
import { SecurityCellActionType } from '../../constants';
|
||||
import type { StartServices } from '../../../types';
|
||||
import type { SecurityCellAction } from '../../types';
|
||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
|
||||
const severityField = 'kibana.alert.severity';
|
||||
const statusField = 'kibana.alert.workflow_status';
|
||||
|
||||
export const getToastMessage = ({ queryMatch: { value }, and = [] }: DataProvider) => {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
const fieldValue = Array.isArray(value) ? value.join(', ') : value.toString();
|
||||
|
||||
const descriptors = and.reduce<string[]>((msg, { queryMatch }) => {
|
||||
if (Array.isArray(queryMatch.value)) {
|
||||
return msg;
|
||||
}
|
||||
if (queryMatch.field === severityField) {
|
||||
msg.push(SEVERITY(queryMatch.value.toString()));
|
||||
}
|
||||
if (queryMatch.field === statusField) {
|
||||
msg.push(queryMatch.value.toString());
|
||||
}
|
||||
return msg;
|
||||
}, []);
|
||||
|
||||
return ALERTS_COUNT(fieldValue, descriptors.join(', '));
|
||||
};
|
||||
|
||||
export const createAddToNewTimelineCellActionFactory = createCellActionFactory(
|
||||
({
|
||||
store,
|
||||
services,
|
||||
}: {
|
||||
store: SecurityAppStore;
|
||||
services: StartServices;
|
||||
}): CellActionTemplate<SecurityCellAction> => {
|
||||
const { notifications: notificationsService } = services;
|
||||
|
||||
return {
|
||||
type: SecurityCellActionType.ADD_TO_TIMELINE,
|
||||
getIconType: () => ADD_TO_TIMELINE_ICON,
|
||||
getDisplayName: () => ADD_TO_NEW_TIMELINE,
|
||||
getDisplayNameTooltip: () => ADD_TO_NEW_TIMELINE,
|
||||
isCompatible: async ({ field }) =>
|
||||
fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type),
|
||||
execute: async ({ field, metadata }) => {
|
||||
const dataProviders =
|
||||
createDataProviders({
|
||||
contextId: TimelineId.active,
|
||||
fieldType: field.type,
|
||||
values: field.value,
|
||||
field: field.name,
|
||||
negate: metadata?.negateFilters === true,
|
||||
}) ?? [];
|
||||
|
||||
for (const andFilter of metadata?.andFilters ?? []) {
|
||||
const andDataProviders =
|
||||
createDataProviders({
|
||||
contextId: TimelineId.active,
|
||||
field: andFilter.field,
|
||||
values: andFilter.value,
|
||||
}) ?? [];
|
||||
if (andDataProviders) {
|
||||
for (const dataProvider of dataProviders) {
|
||||
dataProvider.and.push(...andDataProviders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dataProviders.length > 0) {
|
||||
// clear timeline
|
||||
store.dispatch(
|
||||
timelineActions.createTimeline({
|
||||
...timelineDefaults,
|
||||
id: TimelineId.active,
|
||||
})
|
||||
);
|
||||
store.dispatch(addProvider({ id: TimelineId.active, providers: dataProviders }));
|
||||
notificationsService.toasts.addSuccess({
|
||||
title: ADD_TO_TIMELINE_SUCCESS_TITLE(getToastMessage(dataProviders[0])),
|
||||
});
|
||||
} else {
|
||||
notificationsService.toasts.addWarning({
|
||||
title: ADD_TO_TIMELINE_FAILED_TITLE,
|
||||
text: ADD_TO_TIMELINE_FAILED_TEXT,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
|
@ -15,6 +15,29 @@ export const ADD_TO_TIMELINE = i18n.translate(
|
|||
defaultMessage: 'Add to timeline',
|
||||
}
|
||||
);
|
||||
export const ADD_TO_NEW_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.actions.cellValue.addToNewTimeline.displayName',
|
||||
{
|
||||
defaultMessage: 'Investigate in timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEVERITY = (level: string) =>
|
||||
i18n.translate('xpack.securitySolution.actions.addToTimeline.severityLevel', {
|
||||
values: { level },
|
||||
defaultMessage: `{level} severity`,
|
||||
});
|
||||
|
||||
export const ALERTS_COUNT = (entity: string, description: string) =>
|
||||
description !== ''
|
||||
? i18n.translate('xpack.securitySolution.actions.addToTimeline.descriptiveAlertsCountMessage', {
|
||||
values: { description, entity },
|
||||
defaultMessage: '{description} alerts from {entity}',
|
||||
})
|
||||
: i18n.translate('xpack.securitySolution.actions.addToTimeline.alertsCountMessage', {
|
||||
values: { entity },
|
||||
defaultMessage: '{entity} alerts',
|
||||
});
|
||||
|
||||
export const ADD_TO_TIMELINE_SUCCESS_TITLE = (value: string) =>
|
||||
i18n.translate('xpack.securitySolution.actions.addToTimeline.addedFieldMessage', {
|
||||
|
|
|
@ -6,4 +6,5 @@
|
|||
*/
|
||||
|
||||
export { createAddToTimelineCellActionFactory } from './cell_action/add_to_timeline';
|
||||
export { createAddToNewTimelineCellActionFactory } from './cell_action/add_to_new_timeline';
|
||||
export { createAddToTimelineLensAction } from './lens/add_to_timeline';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
export enum SecurityCellActionsTrigger {
|
||||
DEFAULT = 'security-default-cellActions',
|
||||
DETAILS_FLYOUT = 'security-detailsFlyout-cellActions',
|
||||
ALERTS_COUNT = 'security-alertsCount-cellActions',
|
||||
}
|
||||
|
||||
export enum SecurityCellActionType {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { createFilterInCellActionFactory, createFilterOutCellActionFactory } fro
|
|||
import {
|
||||
createAddToTimelineLensAction,
|
||||
createAddToTimelineCellActionFactory,
|
||||
createAddToNewTimelineCellActionFactory,
|
||||
} from './add_to_timeline';
|
||||
import { createShowTopNCellActionFactory } from './show_top_n';
|
||||
import {
|
||||
|
@ -52,6 +53,7 @@ const registerCellActions = (
|
|||
filterIn: createFilterInCellActionFactory({ store, services }),
|
||||
filterOut: createFilterOutCellActionFactory({ store, services }),
|
||||
addToTimeline: createAddToTimelineCellActionFactory({ store, services }),
|
||||
addToNewTimeline: createAddToNewTimelineCellActionFactory({ store, services }),
|
||||
showTopN: createShowTopNCellActionFactory({ store, history, services }),
|
||||
copyToClipboard: createCopyToClipboardCellActionFactory({ services }),
|
||||
toggleColumn: createToggleColumnCellActionFactory({ store }),
|
||||
|
@ -77,6 +79,13 @@ const registerCellActions = (
|
|||
],
|
||||
services,
|
||||
});
|
||||
|
||||
registerCellActionsTrigger({
|
||||
triggerId: SecurityCellActionsTrigger.ALERTS_COUNT,
|
||||
cellActions,
|
||||
actionsOrder: ['addToNewTimeline'],
|
||||
services,
|
||||
});
|
||||
};
|
||||
|
||||
const registerCellActionsTrigger = ({
|
||||
|
@ -95,8 +104,10 @@ const registerCellActionsTrigger = ({
|
|||
|
||||
actionsOrder.forEach((actionName, order) => {
|
||||
const actionFactory = cellActions[actionName];
|
||||
const action = actionFactory({ id: `${triggerId}-${actionName}`, order });
|
||||
if (actionFactory) {
|
||||
const action = actionFactory({ id: `${triggerId}-${actionName}`, order });
|
||||
|
||||
uiActions.addTriggerAction(triggerId, enhanceActionWithTelemetry(action, services));
|
||||
uiActions.addTriggerAction(triggerId, enhanceActionWithTelemetry(action, services));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,6 +6,12 @@
|
|||
*/
|
||||
|
||||
import type { CellAction, CellActionExecutionContext, CellActionFactory } from '@kbn/cell-actions';
|
||||
import type { QueryOperator } from '../../common/types';
|
||||
export interface AndFilter {
|
||||
field: string;
|
||||
value: string | string[];
|
||||
operator?: QueryOperator;
|
||||
}
|
||||
|
||||
export interface SecurityMetadata extends Record<string, unknown> {
|
||||
/**
|
||||
|
@ -35,6 +41,11 @@ export interface SecurityMetadata extends Record<string, unknown> {
|
|||
*/
|
||||
component: string;
|
||||
};
|
||||
/**
|
||||
* `metadata.andFilters` is used by the addToTimelineAction to add
|
||||
* an "and" query to the main data provider
|
||||
*/
|
||||
andFilters?: AndFilter[];
|
||||
}
|
||||
|
||||
export interface SecurityCellActionExecutionContext extends CellActionExecutionContext {
|
||||
|
@ -42,13 +53,15 @@ export interface SecurityCellActionExecutionContext extends CellActionExecutionC
|
|||
}
|
||||
export type SecurityCellAction = CellAction<SecurityCellActionExecutionContext>;
|
||||
|
||||
// All security cell actions names
|
||||
export type SecurityCellActionName =
|
||||
| 'filterIn'
|
||||
| 'filterOut'
|
||||
| 'addToTimeline'
|
||||
| 'showTopN'
|
||||
| 'copyToClipboard'
|
||||
| 'toggleColumn';
|
||||
export interface SecurityCellActions {
|
||||
filterIn?: CellActionFactory;
|
||||
filterOut?: CellActionFactory;
|
||||
addToTimeline?: CellActionFactory;
|
||||
addToNewTimeline?: CellActionFactory;
|
||||
showTopN?: CellActionFactory;
|
||||
copyToClipboard?: CellActionFactory;
|
||||
toggleColumn?: CellActionFactory;
|
||||
}
|
||||
|
||||
export type SecurityCellActions = Record<SecurityCellActionName, CellActionFactory>;
|
||||
// All security cell actions names
|
||||
export type SecurityCellActionName = keyof SecurityCellActions;
|
||||
|
|
|
@ -10,7 +10,7 @@ exports[`entity_draggable renders correctly against snapshot 1`] = `
|
|||
"value": "entity-value",
|
||||
}
|
||||
}
|
||||
mode="hover"
|
||||
mode="hover-down"
|
||||
triggerId="security-default-cellActions"
|
||||
visibleCellActions={5}
|
||||
>
|
||||
|
|
|
@ -23,7 +23,7 @@ export const EntityComponent: React.FC<Props> = ({ entityName, entityValue }) =>
|
|||
aggregatable: true,
|
||||
}}
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
>
|
||||
{`${entityName}: "${entityValue}"`}
|
||||
|
|
|
@ -10,7 +10,7 @@ exports[`draggable_score renders correctly against snapshot 1`] = `
|
|||
"value": "du",
|
||||
}
|
||||
}
|
||||
mode="hover"
|
||||
mode="hover-down"
|
||||
triggerId="security-default-cellActions"
|
||||
visibleCellActions={5}
|
||||
>
|
||||
|
@ -28,7 +28,7 @@ exports[`draggable_score renders correctly against snapshot when the index is no
|
|||
"value": "du",
|
||||
}
|
||||
}
|
||||
mode="hover"
|
||||
mode="hover-down"
|
||||
triggerId="security-default-cellActions"
|
||||
visibleCellActions={5}
|
||||
>
|
||||
|
|
|
@ -26,7 +26,7 @@ export const ScoreComponent = ({
|
|||
|
||||
return (
|
||||
<SecurityCellActions
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
field={{
|
||||
name: score.entityName,
|
||||
value: score.entityValue,
|
||||
|
|
|
@ -45,7 +45,7 @@ export const getRowItemsWithActions = ({
|
|||
return (
|
||||
<SecurityCellActions
|
||||
key={id}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
|
@ -56,7 +56,7 @@ export const getAlertsTypeTableColumns = (
|
|||
<EuiHealth color={ALERT_TYPE_COLOR[type as AlertType]}>
|
||||
<EuiText grow={false} size="xs">
|
||||
<SecurityCellActions
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={4}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
|
@ -38,7 +38,7 @@ export const getHostRiskScoreColumns = ({
|
|||
if (hostName != null && hostName.length > 0) {
|
||||
return (
|
||||
<SecurityCellActions
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
|
@ -38,7 +38,7 @@ export const getHostsColumns = (
|
|||
if (hostName != null && hostName.length > 0) {
|
||||
return (
|
||||
<SecurityCellActions
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
@ -96,7 +96,7 @@ export const getHostsColumns = (
|
|||
if (hostOsName != null) {
|
||||
return (
|
||||
<SecurityCellActions
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
@ -123,7 +123,7 @@ export const getHostsColumns = (
|
|||
if (hostOsVersion != null) {
|
||||
return (
|
||||
<SecurityCellActions
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
|
@ -42,7 +42,7 @@ export const getNetworkDnsColumns = (): NetworkDnsColumns => [
|
|||
return (
|
||||
<SecurityCellActions
|
||||
key={escapeDataProviderId(`networkDns-table--name-${dnsName}`)}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
|
@ -60,7 +60,7 @@ export const getNetworkTopCountriesColumns = (
|
|||
return (
|
||||
<SecurityCellActions
|
||||
key={id}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
|
@ -68,7 +68,7 @@ export const getNetworkTopNFlowColumns = (
|
|||
<>
|
||||
<SecurityCellActions
|
||||
key={id}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
@ -85,7 +85,7 @@ export const getNetworkTopNFlowColumns = (
|
|||
{geo && (
|
||||
<SecurityCellActions
|
||||
key={`${id}-${geo}`}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
|
@ -183,7 +183,7 @@ const NetworkDetailsComponent: React.FC = () => {
|
|||
title={
|
||||
<SecurityCellActions
|
||||
field={{ type: 'ip', value: ip, name: `${flowTarget}.ip` }}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
>
|
||||
|
|
|
@ -41,7 +41,7 @@ export const getUserRiskScoreColumns = ({
|
|||
return (
|
||||
<SecurityCellActions
|
||||
key={id}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
|
||||
import { CellActionsMode } from '@kbn/cell-actions';
|
||||
import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
|
@ -33,6 +34,10 @@ import * as i18n from '../translations';
|
|||
import { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils';
|
||||
import type { HostAlertsItem } from './use_host_alerts_items';
|
||||
import { useHostAlertsItems } from './use_host_alerts_items';
|
||||
import {
|
||||
SecurityCellActions,
|
||||
SecurityCellActionsTrigger,
|
||||
} from '../../../../common/components/cell_actions';
|
||||
|
||||
interface HostAlertsTableProps {
|
||||
signalIndexName: string | null;
|
||||
|
@ -143,13 +148,27 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.ALERTS_TEXT,
|
||||
'data-test-subj': 'hostSeverityAlertsTable-totalAlerts',
|
||||
render: (totalAlerts: number, { hostName }) => (
|
||||
<EuiLink
|
||||
data-test-subj="hostSeverityAlertsTable-totalAlertsLink"
|
||||
disabled={totalAlerts === 0}
|
||||
onClick={() => handleClick({ hostName })}
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'host.name',
|
||||
value: hostName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
|
||||
}}
|
||||
>
|
||||
<FormattedCount count={totalAlerts} />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
data-test-subj="hostSeverityAlertsTable-totalAlertsLink"
|
||||
disabled={totalAlerts === 0}
|
||||
onClick={() => handleClick({ hostName })}
|
||||
>
|
||||
<FormattedCount count={totalAlerts} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -157,13 +176,30 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.STATUS_CRITICAL_LABEL,
|
||||
render: (count: number, { hostName }) => (
|
||||
<EuiHealth data-test-subj="hostSeverityAlertsTable-critical" color={SEVERITY_COLOR.critical}>
|
||||
<EuiLink
|
||||
data-test-subj="hostSeverityAlertsTable-criticalLink"
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ hostName, severity: 'critical' })}
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'host.name',
|
||||
value: hostName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [
|
||||
{ field: 'kibana.alert.severity', value: 'critical' },
|
||||
{ field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
data-test-subj="hostSeverityAlertsTable-criticalLink"
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ hostName, severity: 'critical' })}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
|
@ -172,9 +208,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.STATUS_HIGH_LABEL,
|
||||
render: (count: number, { hostName }) => (
|
||||
<EuiHealth data-test-subj="hostSeverityAlertsTable-high" color={SEVERITY_COLOR.high}>
|
||||
<EuiLink disabled={count === 0} onClick={() => handleClick({ hostName, severity: 'high' })}>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'host.name',
|
||||
value: hostName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [
|
||||
{ field: 'kibana.alert.severity', value: 'high' },
|
||||
{ field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<EuiLink
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ hostName, severity: 'high' })}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
|
@ -183,12 +239,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.STATUS_MEDIUM_LABEL,
|
||||
render: (count: number, { hostName }) => (
|
||||
<EuiHealth data-test-subj="hostSeverityAlertsTable-medium" color={SEVERITY_COLOR.medium}>
|
||||
<EuiLink
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ hostName, severity: 'medium' })}
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'host.name',
|
||||
value: hostName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [
|
||||
{ field: 'kibana.alert.severity', value: 'medium' },
|
||||
{ field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ hostName, severity: 'medium' })}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
|
@ -197,9 +270,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.STATUS_LOW_LABEL,
|
||||
render: (count: number, { hostName }) => (
|
||||
<EuiHealth data-test-subj="hostSeverityAlertsTable-low" color={SEVERITY_COLOR.low}>
|
||||
<EuiLink disabled={count === 0} onClick={() => handleClick({ hostName, severity: 'low' })}>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'host.name',
|
||||
value: hostName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [
|
||||
{ field: 'kibana.alert.severity', value: 'low' },
|
||||
{ field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<EuiLink
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ hostName, severity: 'low' })}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -21,6 +21,8 @@ import {
|
|||
import { FormattedRelative } from '@kbn/i18n-react';
|
||||
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
|
||||
import { CellActionsMode } from '@kbn/cell-actions';
|
||||
import { SecurityCellActionsTrigger } from '../../../../actions/constants';
|
||||
import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
|
||||
|
@ -36,6 +38,7 @@ import { HoverVisibilityContainer } from '../../../../common/components/hover_vi
|
|||
import { BUTTON_CLASS as INSPECT_BUTTON_CLASS } from '../../../../common/components/inspect';
|
||||
import { LastUpdatedAt } from '../../../../common/components/last_updated_at';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
import { SecurityCellActions } from '../../../../common/components/cell_actions';
|
||||
|
||||
export interface RuleAlertsTableProps {
|
||||
signalIndexName: string | null;
|
||||
|
@ -95,13 +98,27 @@ export const getTableColumns: GetTableColumns = ({
|
|||
name: i18n.RULE_ALERTS_COLUMN_ALERT_COUNT,
|
||||
'data-test-subj': 'severityRuleAlertsTable-alertCount',
|
||||
render: (alertCount: number, { name }) => (
|
||||
<EuiLink
|
||||
data-test-subj="severityRuleAlertsTable-alertCountLink"
|
||||
disabled={alertCount === 0}
|
||||
onClick={() => openRuleInAlertsPage(name)}
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: ALERT_RULE_NAME,
|
||||
value: name,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
|
||||
}}
|
||||
>
|
||||
<FormattedCount count={alertCount} />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
data-test-subj="severityRuleAlertsTable-alertCountLink"
|
||||
disabled={alertCount === 0}
|
||||
onClick={() => openRuleInAlertsPage(name)}
|
||||
>
|
||||
<FormattedCount count={alertCount} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
|
@ -20,6 +20,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
|
||||
import { CellActionsMode } from '@kbn/cell-actions';
|
||||
import { SecurityCellActionsTrigger } from '../../../../actions/constants';
|
||||
import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
|
@ -32,6 +34,7 @@ import * as i18n from '../translations';
|
|||
import { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils';
|
||||
import type { UserAlertsItem } from './use_user_alerts_items';
|
||||
import { useUserAlertsItems } from './use_user_alerts_items';
|
||||
import { SecurityCellActions } from '../../../../common/components/cell_actions';
|
||||
|
||||
interface UserAlertsTableProps {
|
||||
signalIndexName: string | null;
|
||||
|
@ -142,13 +145,27 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.ALERTS_TEXT,
|
||||
'data-test-subj': 'userSeverityAlertsTable-totalAlerts',
|
||||
render: (totalAlerts: number, { userName }) => (
|
||||
<EuiLink
|
||||
data-test-subj="userSeverityAlertsTable-totalAlertsLink"
|
||||
disabled={totalAlerts === 0}
|
||||
onClick={() => handleClick({ userName })}
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'user.name',
|
||||
value: userName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
|
||||
}}
|
||||
>
|
||||
<FormattedCount count={totalAlerts} />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
data-test-subj="userSeverityAlertsTable-totalAlertsLink"
|
||||
disabled={totalAlerts === 0}
|
||||
onClick={() => handleClick({ userName })}
|
||||
>
|
||||
<FormattedCount count={totalAlerts} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -156,13 +173,30 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.STATUS_CRITICAL_LABEL,
|
||||
render: (count: number, { userName }) => (
|
||||
<EuiHealth data-test-subj="userSeverityAlertsTable-critical" color={SEVERITY_COLOR.critical}>
|
||||
<EuiLink
|
||||
data-test-subj="userSeverityAlertsTable-criticalLink"
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ userName, severity: 'critical' })}
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'user.name',
|
||||
value: userName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [
|
||||
{ field: 'kibana.alert.severity', value: 'critical' },
|
||||
{ field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
data-test-subj="userSeverityAlertsTable-criticalLink"
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ userName, severity: 'critical' })}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
|
@ -171,9 +205,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.STATUS_HIGH_LABEL,
|
||||
render: (count: number, { userName }) => (
|
||||
<EuiHealth data-test-subj="userSeverityAlertsTable-high" color={SEVERITY_COLOR.high}>
|
||||
<EuiLink disabled={count === 0} onClick={() => handleClick({ userName, severity: 'high' })}>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'user.name',
|
||||
value: userName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [
|
||||
{ field: 'kibana.alert.severity', value: 'high' },
|
||||
{ field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<EuiLink
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ userName, severity: 'high' })}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
|
@ -182,12 +236,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.STATUS_MEDIUM_LABEL,
|
||||
render: (count: number, { userName }) => (
|
||||
<EuiHealth data-test-subj="userSeverityAlertsTable-medium" color={SEVERITY_COLOR.medium}>
|
||||
<EuiLink
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ userName, severity: 'medium' })}
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'user.name',
|
||||
value: userName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [
|
||||
{ field: 'kibana.alert.severity', value: 'medium' },
|
||||
{ field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ userName, severity: 'medium' })}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
|
@ -196,9 +267,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
|||
name: i18n.STATUS_LOW_LABEL,
|
||||
render: (count: number, { userName }) => (
|
||||
<EuiHealth data-test-subj="userSeverityAlertsTable-low" color={SEVERITY_COLOR.low}>
|
||||
<EuiLink disabled={count === 0} onClick={() => handleClick({ userName, severity: 'low' })}>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: 'user.name',
|
||||
value: userName,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [
|
||||
{ field: 'kibana.alert.severity', value: 'low' },
|
||||
{ field: 'kibana.alert.workflow_status', value: 'open' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<EuiLink
|
||||
disabled={count === 0}
|
||||
onClick={() => handleClick({ userName, severity: 'low' })}
|
||||
>
|
||||
<FormattedCount count={count} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -140,17 +140,31 @@ export const getRiskScoreColumns = (
|
|||
truncateText: false,
|
||||
mobileOptions: { show: true },
|
||||
render: (alertCount: number, risk) => (
|
||||
<EuiLink
|
||||
data-test-subj="risk-score-alerts"
|
||||
disabled={alertCount === 0}
|
||||
onClick={() =>
|
||||
openEntityOnAlertsPage(
|
||||
riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name
|
||||
)
|
||||
}
|
||||
<SecurityCellActions
|
||||
field={{
|
||||
name: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
|
||||
value: riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name,
|
||||
type: 'keyword',
|
||||
aggregatable: true,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
|
||||
metadata={{
|
||||
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
|
||||
}}
|
||||
>
|
||||
<FormattedCount count={alertCount} />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
data-test-subj="risk-score-alerts"
|
||||
disabled={alertCount === 0}
|
||||
onClick={() =>
|
||||
openEntityOnAlertsPage(
|
||||
riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedCount count={alertCount} />
|
||||
</EuiLink>
|
||||
</SecurityCellActions>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { EntityAnalyticsRiskScores } from '.';
|
||||
import type { UserRiskScore } from '../../../../../common/search_strategy';
|
||||
import { RiskScoreEntity, RiskSeverity } from '../../../../../common/search_strategy';
|
||||
import type { SeverityCount } from '../../../../explore/components/risk_score/severity/types';
|
||||
import { useRiskScore, useRiskScoreKpi } from '../../../../explore/containers/risk_score';
|
||||
|
@ -146,17 +145,17 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
expect(queryByTestId('entity_analytics_content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders alerts count', () => {
|
||||
it('renders alerts count', async () => {
|
||||
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
|
||||
mockUseRiskScoreKpi.mockReturnValue({
|
||||
severityCount: mockSeverityCount,
|
||||
loading: false,
|
||||
});
|
||||
const alertsCount = 999;
|
||||
const data: UserRiskScore[] = [
|
||||
const data = [
|
||||
{
|
||||
'@timestamp': '1234567899',
|
||||
user: {
|
||||
[riskEntity]: {
|
||||
name: 'testUsermame',
|
||||
risk: {
|
||||
rule_risks: [],
|
||||
|
@ -176,10 +175,12 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString());
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to alerts page with filters when alerts count is clicked', () => {
|
||||
it('navigates to alerts page with filters when alerts count is clicked', async () => {
|
||||
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
|
||||
mockUseRiskScoreKpi.mockReturnValue({
|
||||
severityCount: mockSeverityCount,
|
||||
|
@ -211,13 +212,15 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
|
||||
fireEvent.click(getByTestId('risk-score-alerts'));
|
||||
|
||||
expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([
|
||||
{
|
||||
title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User',
|
||||
fieldName: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
|
||||
selectedOptions: [name],
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([
|
||||
{
|
||||
title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User',
|
||||
fieldName: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
|
||||
selectedOptions: [name],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -317,7 +317,7 @@ export const MoreContainer = React.memo<MoreContainerProps>(
|
|||
<EuiFlexItem key={id}>
|
||||
<SecurityCellActions
|
||||
key={id}
|
||||
mode={CellActionsMode.HOVER}
|
||||
mode={CellActionsMode.HOVER_DOWN}
|
||||
visibleCellActions={5}
|
||||
showActionTooltips
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue