mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -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
|
```JSX
|
||||||
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
|
<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
|
Hover me
|
||||||
</CellActions>
|
</CellActions>
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
|
|
|
@ -50,8 +50,8 @@ export const DefaultWithControls = CellActionsTemplate.bind({});
|
||||||
|
|
||||||
DefaultWithControls.argTypes = {
|
DefaultWithControls.argTypes = {
|
||||||
mode: {
|
mode: {
|
||||||
options: [CellActionsMode.HOVER, CellActionsMode.INLINE],
|
options: [CellActionsMode.HOVER_DOWN, CellActionsMode.INLINE],
|
||||||
defaultValue: CellActionsMode.HOVER,
|
defaultValue: CellActionsMode.HOVER_DOWN,
|
||||||
control: {
|
control: {
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
},
|
},
|
||||||
|
@ -72,8 +72,14 @@ export const CellActionInline = ({}: {}) => (
|
||||||
</CellActions>
|
</CellActions>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CellActionHoverPopup = ({}: {}) => (
|
export const CellActionHoverPopoverDown = ({}: {}) => (
|
||||||
<CellActions mode={CellActionsMode.HOVER} triggerId={TRIGGER_ID} field={FIELD}>
|
<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
|
Hover me
|
||||||
</CellActions>
|
</CellActions>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,6 +15,11 @@ import { CellActionsProvider } from '../context/cell_actions_context';
|
||||||
const TRIGGER_ID = 'test-trigger-id';
|
const TRIGGER_ID = 'test-trigger-id';
|
||||||
const FIELD = { name: 'name', value: '123', type: 'text' };
|
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', () => {
|
describe('CellActions', () => {
|
||||||
it('renders', async () => {
|
it('renders', async () => {
|
||||||
const getActionsPromise = Promise.resolve([]);
|
const getActionsPromise = Promise.resolve([]);
|
||||||
|
@ -54,13 +59,13 @@ describe('CellActions', () => {
|
||||||
expect(queryByTestId('inlineActions')).toBeInTheDocument();
|
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 getActionsPromise = Promise.resolve([]);
|
||||||
const getActions = () => getActionsPromise;
|
const getActions = () => getActionsPromise;
|
||||||
|
|
||||||
const { queryByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<CellActions mode={CellActionsMode.HOVER} triggerId={TRIGGER_ID} field={FIELD}>
|
<CellActions mode={CellActionsMode.HOVER_DOWN} triggerId={TRIGGER_ID} field={FIELD}>
|
||||||
Field value
|
Field value
|
||||||
</CellActions>
|
</CellActions>
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
|
@ -70,6 +75,27 @@ describe('CellActions', () => {
|
||||||
await getActionsPromise;
|
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]
|
[field, triggerId, metadata]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const anchorPosition = useMemo(
|
||||||
|
() => (mode === CellActionsMode.HOVER_DOWN ? 'downCenter' : 'rightCenter'),
|
||||||
|
[mode]
|
||||||
|
);
|
||||||
|
|
||||||
const dataTestSubj = `cellActions-renderContent-${field.name}`;
|
const dataTestSubj = `cellActions-renderContent-${field.name}`;
|
||||||
if (mode === CellActionsMode.HOVER) {
|
if (mode === CellActionsMode.HOVER_DOWN || mode === CellActionsMode.HOVER_RIGHT) {
|
||||||
return (
|
return (
|
||||||
<div className={className} ref={nodeRef} data-test-subj={dataTestSubj}>
|
<div className={className} ref={nodeRef} data-test-subj={dataTestSubj}>
|
||||||
<HoverActionsPopover
|
<HoverActionsPopover
|
||||||
|
anchorPosition={anchorPosition}
|
||||||
actionContext={actionContext}
|
actionContext={actionContext}
|
||||||
showActionTooltips={showActionTooltips}
|
showActionTooltips={showActionTooltips}
|
||||||
visibleCellActions={visibleCellActions}
|
visibleCellActions={visibleCellActions}
|
||||||
|
@ -65,6 +71,7 @@ export const CellActions: React.FC<CellActionsProps> = ({
|
||||||
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<InlineActions
|
<InlineActions
|
||||||
|
anchorPosition={anchorPosition}
|
||||||
actionContext={actionContext}
|
actionContext={actionContext}
|
||||||
showActionTooltips={showActionTooltips}
|
showActionTooltips={showActionTooltips}
|
||||||
visibleCellActions={visibleCellActions}
|
visibleCellActions={visibleCellActions}
|
||||||
|
|
|
@ -12,17 +12,17 @@ import { makeAction, makeActionContext } from '../mocks/helpers';
|
||||||
import { ExtraActionsPopOver, ExtraActionsPopOverWithAnchor } from './extra_actions_popover';
|
import { ExtraActionsPopOver, ExtraActionsPopOverWithAnchor } from './extra_actions_popover';
|
||||||
|
|
||||||
const actionContext = makeActionContext();
|
const actionContext = makeActionContext();
|
||||||
|
const defaultProps = {
|
||||||
|
anchorPosition: 'rightCenter' as const,
|
||||||
|
actionContext,
|
||||||
|
isOpen: false,
|
||||||
|
closePopOver: () => {},
|
||||||
|
actions: [],
|
||||||
|
button: <span />,
|
||||||
|
};
|
||||||
describe('ExtraActionsPopOver', () => {
|
describe('ExtraActionsPopOver', () => {
|
||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(<ExtraActionsPopOver {...defaultProps} />);
|
||||||
<ExtraActionsPopOver
|
|
||||||
actionContext={actionContext}
|
|
||||||
isOpen={false}
|
|
||||||
closePopOver={() => {}}
|
|
||||||
actions={[]}
|
|
||||||
button={<span />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(queryByTestId('extraActionsPopOver')).toBeInTheDocument();
|
expect(queryByTestId('extraActionsPopOver')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -33,11 +33,10 @@ describe('ExtraActionsPopOver', () => {
|
||||||
const action = { ...makeAction('test-action'), execute: executeAction };
|
const action = { ...makeAction('test-action'), execute: executeAction };
|
||||||
const { getByLabelText } = render(
|
const { getByLabelText } = render(
|
||||||
<ExtraActionsPopOver
|
<ExtraActionsPopOver
|
||||||
actionContext={actionContext}
|
{...defaultProps}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
closePopOver={closePopOver}
|
closePopOver={closePopOver}
|
||||||
actions={[action]}
|
actions={[action]}
|
||||||
button={<span />}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -56,13 +55,7 @@ describe('ExtraActionsPopOverWithAnchor', () => {
|
||||||
|
|
||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<ExtraActionsPopOverWithAnchor
|
<ExtraActionsPopOverWithAnchor {...defaultProps} anchorRef={{ current: anchorElement }} />
|
||||||
actionContext={actionContext}
|
|
||||||
isOpen={false}
|
|
||||||
closePopOver={() => {}}
|
|
||||||
actions={[]}
|
|
||||||
anchorRef={{ current: anchorElement }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(queryByTestId('extraActionsPopOverWithAnchor')).toBeInTheDocument();
|
expect(queryByTestId('extraActionsPopOverWithAnchor')).toBeInTheDocument();
|
||||||
|
@ -74,7 +67,7 @@ describe('ExtraActionsPopOverWithAnchor', () => {
|
||||||
const action = { ...makeAction('test-action'), execute: executeAction };
|
const action = { ...makeAction('test-action'), execute: executeAction };
|
||||||
const { getByLabelText } = render(
|
const { getByLabelText } = render(
|
||||||
<ExtraActionsPopOverWithAnchor
|
<ExtraActionsPopOverWithAnchor
|
||||||
actionContext={actionContext}
|
{...defaultProps}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
closePopOver={closePopOver}
|
closePopOver={closePopOver}
|
||||||
actions={[action]}
|
actions={[action]}
|
||||||
|
|
|
@ -24,6 +24,7 @@ const euiContextMenuItemCSS = css`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface ActionsPopOverProps {
|
interface ActionsPopOverProps {
|
||||||
|
anchorPosition: 'rightCenter' | 'downCenter';
|
||||||
actionContext: CellActionExecutionContext;
|
actionContext: CellActionExecutionContext;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
closePopOver: () => void;
|
closePopOver: () => void;
|
||||||
|
@ -32,6 +33,7 @@ interface ActionsPopOverProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExtraActionsPopOver: React.FC<ActionsPopOverProps> = ({
|
export const ExtraActionsPopOver: React.FC<ActionsPopOverProps> = ({
|
||||||
|
anchorPosition,
|
||||||
actions,
|
actions,
|
||||||
actionContext,
|
actionContext,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
@ -43,7 +45,7 @@ export const ExtraActionsPopOver: React.FC<ActionsPopOverProps> = ({
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
closePopover={closePopOver}
|
closePopover={closePopOver}
|
||||||
panelPaddingSize="xs"
|
panelPaddingSize="xs"
|
||||||
anchorPosition={'downCenter'}
|
anchorPosition={anchorPosition}
|
||||||
hasArrow
|
hasArrow
|
||||||
repositionOnScroll
|
repositionOnScroll
|
||||||
ownFocus
|
ownFocus
|
||||||
|
@ -59,11 +61,15 @@ export const ExtraActionsPopOver: React.FC<ActionsPopOverProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ExtraActionsPopOverWithAnchorProps
|
interface ExtraActionsPopOverWithAnchorProps
|
||||||
extends Pick<ActionsPopOverProps, 'actionContext' | 'closePopOver' | 'isOpen' | 'actions'> {
|
extends Pick<
|
||||||
|
ActionsPopOverProps,
|
||||||
|
'anchorPosition' | 'actionContext' | 'closePopOver' | 'isOpen' | 'actions'
|
||||||
|
> {
|
||||||
anchorRef: React.RefObject<HTMLElement>;
|
anchorRef: React.RefObject<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExtraActionsPopOverWithAnchor = ({
|
export const ExtraActionsPopOverWithAnchor = ({
|
||||||
|
anchorPosition,
|
||||||
anchorRef,
|
anchorRef,
|
||||||
actionContext,
|
actionContext,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
@ -77,7 +83,7 @@ export const ExtraActionsPopOverWithAnchor = ({
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
closePopover={closePopOver}
|
closePopover={closePopOver}
|
||||||
panelPaddingSize="xs"
|
panelPaddingSize="xs"
|
||||||
anchorPosition={'downCenter'}
|
anchorPosition={anchorPosition}
|
||||||
hasArrow={false}
|
hasArrow={false}
|
||||||
repositionOnScroll
|
repositionOnScroll
|
||||||
ownFocus
|
ownFocus
|
||||||
|
|
|
@ -13,11 +13,17 @@ import { makeAction } from '../mocks/helpers';
|
||||||
import { CellActionExecutionContext } from '../types';
|
import { CellActionExecutionContext } from '../types';
|
||||||
import { HoverActionsPopover } from './hover_actions_popover';
|
import { HoverActionsPopover } from './hover_actions_popover';
|
||||||
|
|
||||||
describe('HoverActionsPopover', () => {
|
const defaultProps = {
|
||||||
const actionContext = {
|
anchorPosition: 'rightCenter' as const,
|
||||||
|
disabledActionTypes: [],
|
||||||
|
visibleCellActions: 4,
|
||||||
|
actionContext: {
|
||||||
trigger: { id: 'triggerId' },
|
trigger: { id: 'triggerId' },
|
||||||
field: { name: 'fieldName' },
|
field: { name: 'fieldName' },
|
||||||
} as CellActionExecutionContext;
|
} as CellActionExecutionContext,
|
||||||
|
showActionTooltips: false,
|
||||||
|
};
|
||||||
|
describe('HoverActionsPopover', () => {
|
||||||
const TestComponent = () => <span data-test-subj="test-component" />;
|
const TestComponent = () => <span data-test-subj="test-component" />;
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
@ -25,13 +31,7 @@ describe('HoverActionsPopover', () => {
|
||||||
const getActions = () => Promise.resolve([]);
|
const getActions = () => Promise.resolve([]);
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<HoverActionsPopover
|
<HoverActionsPopover {...defaultProps} children={null} />
|
||||||
disabledActionTypes={[]}
|
|
||||||
children={null}
|
|
||||||
visibleCellActions={4}
|
|
||||||
actionContext={actionContext}
|
|
||||||
showActionTooltips={false}
|
|
||||||
/>
|
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
);
|
);
|
||||||
expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument();
|
expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument();
|
||||||
|
@ -44,12 +44,7 @@ describe('HoverActionsPopover', () => {
|
||||||
|
|
||||||
const { queryByLabelText, getByTestId } = render(
|
const { queryByLabelText, getByTestId } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<HoverActionsPopover
|
<HoverActionsPopover {...defaultProps}>
|
||||||
disabledActionTypes={[]}
|
|
||||||
visibleCellActions={4}
|
|
||||||
actionContext={actionContext}
|
|
||||||
showActionTooltips={false}
|
|
||||||
>
|
|
||||||
<TestComponent />
|
<TestComponent />
|
||||||
</HoverActionsPopover>
|
</HoverActionsPopover>
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
|
@ -70,12 +65,7 @@ describe('HoverActionsPopover', () => {
|
||||||
|
|
||||||
const { queryByLabelText, getByTestId } = render(
|
const { queryByLabelText, getByTestId } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<HoverActionsPopover
|
<HoverActionsPopover {...defaultProps}>
|
||||||
disabledActionTypes={[]}
|
|
||||||
visibleCellActions={4}
|
|
||||||
actionContext={actionContext}
|
|
||||||
showActionTooltips={false}
|
|
||||||
>
|
|
||||||
<TestComponent />
|
<TestComponent />
|
||||||
</HoverActionsPopover>
|
</HoverActionsPopover>
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
|
@ -101,12 +91,7 @@ describe('HoverActionsPopover', () => {
|
||||||
|
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<HoverActionsPopover
|
<HoverActionsPopover {...defaultProps} visibleCellActions={1}>
|
||||||
disabledActionTypes={[]}
|
|
||||||
visibleCellActions={1}
|
|
||||||
actionContext={actionContext}
|
|
||||||
showActionTooltips={false}
|
|
||||||
>
|
|
||||||
<TestComponent />
|
<TestComponent />
|
||||||
</HoverActionsPopover>
|
</HoverActionsPopover>
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
|
@ -127,12 +112,7 @@ describe('HoverActionsPopover', () => {
|
||||||
|
|
||||||
const { getByTestId, getByLabelText } = render(
|
const { getByTestId, getByLabelText } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<HoverActionsPopover
|
<HoverActionsPopover {...defaultProps} visibleCellActions={1}>
|
||||||
disabledActionTypes={[]}
|
|
||||||
visibleCellActions={1}
|
|
||||||
actionContext={actionContext}
|
|
||||||
showActionTooltips={false}
|
|
||||||
>
|
|
||||||
<TestComponent />
|
<TestComponent />
|
||||||
</HoverActionsPopover>
|
</HoverActionsPopover>
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
|
@ -162,12 +142,7 @@ describe('HoverActionsPopover', () => {
|
||||||
|
|
||||||
const { getByTestId, queryByLabelText } = render(
|
const { getByTestId, queryByLabelText } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<HoverActionsPopover
|
<HoverActionsPopover {...defaultProps} visibleCellActions={2}>
|
||||||
disabledActionTypes={[]}
|
|
||||||
visibleCellActions={2}
|
|
||||||
actionContext={actionContext}
|
|
||||||
showActionTooltips={false}
|
|
||||||
>
|
|
||||||
<TestComponent />
|
<TestComponent />
|
||||||
</HoverActionsPopover>
|
</HoverActionsPopover>
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
|
@ -191,6 +166,44 @@ describe('HoverActionsPopover', () => {
|
||||||
expect(queryByLabelText('test-action-2')).toBeInTheDocument();
|
expect(queryByLabelText('test-action-2')).toBeInTheDocument();
|
||||||
expect(queryByLabelText('test-action-3')).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>) => {
|
const hoverElement = async (element: Element, waitForChange: () => Promise<unknown>) => {
|
||||||
|
|
|
@ -36,6 +36,7 @@ const hoverContentWrapperCSS = css`
|
||||||
const HOVER_INTENT_DELAY = 100; // ms
|
const HOVER_INTENT_DELAY = 100; // ms
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
anchorPosition: 'downCenter' | 'rightCenter';
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
visibleCellActions: number;
|
visibleCellActions: number;
|
||||||
actionContext: CellActionExecutionContext;
|
actionContext: CellActionExecutionContext;
|
||||||
|
@ -44,6 +45,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HoverActionsPopover: React.FC<Props> = ({
|
export const HoverActionsPopover: React.FC<Props> = ({
|
||||||
|
anchorPosition,
|
||||||
children,
|
children,
|
||||||
visibleCellActions,
|
visibleCellActions,
|
||||||
actionContext,
|
actionContext,
|
||||||
|
@ -115,12 +117,17 @@ export const HoverActionsPopover: React.FC<Props> = ({
|
||||||
);
|
);
|
||||||
}, [onMouseEnter, closeExtraActions, children]);
|
}, [onMouseEnter, closeExtraActions, children]);
|
||||||
|
|
||||||
|
const panelStyle = useMemo(
|
||||||
|
() => (anchorPosition === 'rightCenter' ? { marginTop: euiThemeVars.euiSizeS } : {}),
|
||||||
|
[anchorPosition]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div onMouseLeave={closePopover}>
|
<div onMouseLeave={closePopover}>
|
||||||
<EuiPopover
|
<EuiPopover
|
||||||
panelStyle={PANEL_STYLE}
|
panelStyle={{ ...PANEL_STYLE, ...panelStyle }}
|
||||||
anchorPosition={'downCenter'}
|
anchorPosition={anchorPosition}
|
||||||
button={content}
|
button={content}
|
||||||
closePopover={closePopover}
|
closePopover={closePopover}
|
||||||
hasArrow={false}
|
hasArrow={false}
|
||||||
|
@ -155,6 +162,7 @@ export const HoverActionsPopover: React.FC<Props> = ({
|
||||||
</EuiPopover>
|
</EuiPopover>
|
||||||
</div>
|
</div>
|
||||||
<ExtraActionsPopOverWithAnchor
|
<ExtraActionsPopOverWithAnchor
|
||||||
|
anchorPosition={anchorPosition}
|
||||||
actions={extraActions}
|
actions={extraActions}
|
||||||
anchorRef={contentRef}
|
anchorRef={contentRef}
|
||||||
actionContext={actionContext}
|
actionContext={actionContext}
|
||||||
|
|
|
@ -13,19 +13,20 @@ import { InlineActions } from './inline_actions';
|
||||||
import { CellActionExecutionContext } from '../types';
|
import { CellActionExecutionContext } from '../types';
|
||||||
import { CellActionsProvider } from '../context';
|
import { CellActionsProvider } from '../context';
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
anchorPosition: 'rightCenter' as const,
|
||||||
|
disabledActionTypes: [],
|
||||||
|
visibleCellActions: 5,
|
||||||
|
actionContext: { trigger: { id: 'triggerId' } } as CellActionExecutionContext,
|
||||||
|
showActionTooltips: false,
|
||||||
|
};
|
||||||
describe('InlineActions', () => {
|
describe('InlineActions', () => {
|
||||||
const actionContext = { trigger: { id: 'triggerId' } } as CellActionExecutionContext;
|
|
||||||
it('renders', async () => {
|
it('renders', async () => {
|
||||||
const getActionsPromise = Promise.resolve([]);
|
const getActionsPromise = Promise.resolve([]);
|
||||||
const getActions = () => getActionsPromise;
|
const getActions = () => getActionsPromise;
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<InlineActions
|
<InlineActions {...defaultProps} />
|
||||||
disabledActionTypes={[]}
|
|
||||||
visibleCellActions={5}
|
|
||||||
actionContext={actionContext}
|
|
||||||
showActionTooltips={false}
|
|
||||||
/>
|
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -47,12 +48,7 @@ describe('InlineActions', () => {
|
||||||
const getActions = () => getActionsPromise;
|
const getActions = () => getActionsPromise;
|
||||||
const { queryAllByRole } = render(
|
const { queryAllByRole } = render(
|
||||||
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
<CellActionsProvider getTriggerCompatibleActions={getActions}>
|
||||||
<InlineActions
|
<InlineActions {...defaultProps} />
|
||||||
disabledActionTypes={[]}
|
|
||||||
visibleCellActions={5}
|
|
||||||
actionContext={actionContext}
|
|
||||||
showActionTooltips={false}
|
|
||||||
/>
|
|
||||||
</CellActionsProvider>
|
</CellActionsProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { useLoadActions } from '../hooks/use_load_actions';
|
||||||
|
|
||||||
interface InlineActionsProps {
|
interface InlineActionsProps {
|
||||||
actionContext: CellActionExecutionContext;
|
actionContext: CellActionExecutionContext;
|
||||||
|
anchorPosition: 'rightCenter' | 'downCenter';
|
||||||
showActionTooltips: boolean;
|
showActionTooltips: boolean;
|
||||||
visibleCellActions: number;
|
visibleCellActions: number;
|
||||||
disabledActionTypes: string[];
|
disabledActionTypes: string[];
|
||||||
|
@ -24,6 +25,7 @@ interface InlineActionsProps {
|
||||||
|
|
||||||
export const InlineActions: React.FC<InlineActionsProps> = ({
|
export const InlineActions: React.FC<InlineActionsProps> = ({
|
||||||
actionContext,
|
actionContext,
|
||||||
|
anchorPosition,
|
||||||
showActionTooltips,
|
showActionTooltips,
|
||||||
visibleCellActions,
|
visibleCellActions,
|
||||||
disabledActionTypes,
|
disabledActionTypes,
|
||||||
|
@ -47,10 +49,9 @@ export const InlineActions: React.FC<InlineActionsProps> = ({
|
||||||
data-test-subj="inlineActions"
|
data-test-subj="inlineActions"
|
||||||
className={`inlineActions ${isPopoverOpen ? 'inlineActions-popoverOpen' : ''}`}
|
className={`inlineActions ${isPopoverOpen ? 'inlineActions-popoverOpen' : ''}`}
|
||||||
>
|
>
|
||||||
{visibleActions.map((action, index) => (
|
{visibleActions.map((action) => (
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false} key={`action-item-${action.id}`}>
|
||||||
<ActionItem
|
<ActionItem
|
||||||
key={`action-item-${index}`}
|
|
||||||
action={action}
|
action={action}
|
||||||
actionContext={actionContext}
|
actionContext={actionContext}
|
||||||
showTooltip={showActionTooltips}
|
showTooltip={showActionTooltips}
|
||||||
|
@ -62,6 +63,7 @@ export const InlineActions: React.FC<InlineActionsProps> = ({
|
||||||
<ExtraActionsPopOver
|
<ExtraActionsPopOver
|
||||||
actions={extraActions}
|
actions={extraActions}
|
||||||
actionContext={actionContext}
|
actionContext={actionContext}
|
||||||
|
anchorPosition={anchorPosition}
|
||||||
button={button}
|
button={button}
|
||||||
closePopOver={closePopOver}
|
closePopOver={closePopOver}
|
||||||
isOpen={isPopoverOpen}
|
isOpen={isPopoverOpen}
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const FILTER_CELL_ACTION_TYPE = 'cellAction-filter';
|
||||||
export const COPY_CELL_ACTION_TYPE = 'cellAction-copy';
|
export const COPY_CELL_ACTION_TYPE = 'cellAction-copy';
|
||||||
|
|
||||||
export enum CellActionsMode {
|
export enum CellActionsMode {
|
||||||
HOVER = 'hover',
|
HOVER_DOWN = 'hover-down',
|
||||||
|
HOVER_RIGHT = 'hover-right',
|
||||||
INLINE = 'inline',
|
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',
|
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) =>
|
export const ADD_TO_TIMELINE_SUCCESS_TITLE = (value: string) =>
|
||||||
i18n.translate('xpack.securitySolution.actions.addToTimeline.addedFieldMessage', {
|
i18n.translate('xpack.securitySolution.actions.addToTimeline.addedFieldMessage', {
|
||||||
|
|
|
@ -6,4 +6,5 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { createAddToTimelineCellActionFactory } from './cell_action/add_to_timeline';
|
export { createAddToTimelineCellActionFactory } from './cell_action/add_to_timeline';
|
||||||
|
export { createAddToNewTimelineCellActionFactory } from './cell_action/add_to_new_timeline';
|
||||||
export { createAddToTimelineLensAction } from './lens/add_to_timeline';
|
export { createAddToTimelineLensAction } from './lens/add_to_timeline';
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export enum SecurityCellActionsTrigger {
|
export enum SecurityCellActionsTrigger {
|
||||||
DEFAULT = 'security-default-cellActions',
|
DEFAULT = 'security-default-cellActions',
|
||||||
DETAILS_FLYOUT = 'security-detailsFlyout-cellActions',
|
DETAILS_FLYOUT = 'security-detailsFlyout-cellActions',
|
||||||
|
ALERTS_COUNT = 'security-alertsCount-cellActions',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecurityCellActionType {
|
export enum SecurityCellActionType {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { createFilterInCellActionFactory, createFilterOutCellActionFactory } fro
|
||||||
import {
|
import {
|
||||||
createAddToTimelineLensAction,
|
createAddToTimelineLensAction,
|
||||||
createAddToTimelineCellActionFactory,
|
createAddToTimelineCellActionFactory,
|
||||||
|
createAddToNewTimelineCellActionFactory,
|
||||||
} from './add_to_timeline';
|
} from './add_to_timeline';
|
||||||
import { createShowTopNCellActionFactory } from './show_top_n';
|
import { createShowTopNCellActionFactory } from './show_top_n';
|
||||||
import {
|
import {
|
||||||
|
@ -52,6 +53,7 @@ const registerCellActions = (
|
||||||
filterIn: createFilterInCellActionFactory({ store, services }),
|
filterIn: createFilterInCellActionFactory({ store, services }),
|
||||||
filterOut: createFilterOutCellActionFactory({ store, services }),
|
filterOut: createFilterOutCellActionFactory({ store, services }),
|
||||||
addToTimeline: createAddToTimelineCellActionFactory({ store, services }),
|
addToTimeline: createAddToTimelineCellActionFactory({ store, services }),
|
||||||
|
addToNewTimeline: createAddToNewTimelineCellActionFactory({ store, services }),
|
||||||
showTopN: createShowTopNCellActionFactory({ store, history, services }),
|
showTopN: createShowTopNCellActionFactory({ store, history, services }),
|
||||||
copyToClipboard: createCopyToClipboardCellActionFactory({ services }),
|
copyToClipboard: createCopyToClipboardCellActionFactory({ services }),
|
||||||
toggleColumn: createToggleColumnCellActionFactory({ store }),
|
toggleColumn: createToggleColumnCellActionFactory({ store }),
|
||||||
|
@ -77,6 +79,13 @@ const registerCellActions = (
|
||||||
],
|
],
|
||||||
services,
|
services,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerCellActionsTrigger({
|
||||||
|
triggerId: SecurityCellActionsTrigger.ALERTS_COUNT,
|
||||||
|
cellActions,
|
||||||
|
actionsOrder: ['addToNewTimeline'],
|
||||||
|
services,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerCellActionsTrigger = ({
|
const registerCellActionsTrigger = ({
|
||||||
|
@ -95,8 +104,10 @@ const registerCellActionsTrigger = ({
|
||||||
|
|
||||||
actionsOrder.forEach((actionName, order) => {
|
actionsOrder.forEach((actionName, order) => {
|
||||||
const actionFactory = cellActions[actionName];
|
const actionFactory = cellActions[actionName];
|
||||||
|
if (actionFactory) {
|
||||||
const action = actionFactory({ id: `${triggerId}-${actionName}`, order });
|
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 { 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> {
|
export interface SecurityMetadata extends Record<string, unknown> {
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +41,11 @@ export interface SecurityMetadata extends Record<string, unknown> {
|
||||||
*/
|
*/
|
||||||
component: string;
|
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 {
|
export interface SecurityCellActionExecutionContext extends CellActionExecutionContext {
|
||||||
|
@ -42,13 +53,15 @@ export interface SecurityCellActionExecutionContext extends CellActionExecutionC
|
||||||
}
|
}
|
||||||
export type SecurityCellAction = CellAction<SecurityCellActionExecutionContext>;
|
export type SecurityCellAction = CellAction<SecurityCellActionExecutionContext>;
|
||||||
|
|
||||||
// All security cell actions names
|
export interface SecurityCellActions {
|
||||||
export type SecurityCellActionName =
|
filterIn?: CellActionFactory;
|
||||||
| 'filterIn'
|
filterOut?: CellActionFactory;
|
||||||
| 'filterOut'
|
addToTimeline?: CellActionFactory;
|
||||||
| 'addToTimeline'
|
addToNewTimeline?: CellActionFactory;
|
||||||
| 'showTopN'
|
showTopN?: CellActionFactory;
|
||||||
| 'copyToClipboard'
|
copyToClipboard?: CellActionFactory;
|
||||||
| 'toggleColumn';
|
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",
|
"value": "entity-value",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mode="hover"
|
mode="hover-down"
|
||||||
triggerId="security-default-cellActions"
|
triggerId="security-default-cellActions"
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
>
|
>
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const EntityComponent: React.FC<Props> = ({ entityName, entityValue }) =>
|
||||||
aggregatable: true,
|
aggregatable: true,
|
||||||
}}
|
}}
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
>
|
>
|
||||||
{`${entityName}: "${entityValue}"`}
|
{`${entityName}: "${entityValue}"`}
|
||||||
|
|
|
@ -10,7 +10,7 @@ exports[`draggable_score renders correctly against snapshot 1`] = `
|
||||||
"value": "du",
|
"value": "du",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mode="hover"
|
mode="hover-down"
|
||||||
triggerId="security-default-cellActions"
|
triggerId="security-default-cellActions"
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
>
|
>
|
||||||
|
@ -28,7 +28,7 @@ exports[`draggable_score renders correctly against snapshot when the index is no
|
||||||
"value": "du",
|
"value": "du",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mode="hover"
|
mode="hover-down"
|
||||||
triggerId="security-default-cellActions"
|
triggerId="security-default-cellActions"
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
>
|
>
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const ScoreComponent = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
field={{
|
field={{
|
||||||
name: score.entityName,
|
name: score.entityName,
|
||||||
value: score.entityValue,
|
value: score.entityValue,
|
||||||
|
|
|
@ -45,7 +45,7 @@ export const getRowItemsWithActions = ({
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
key={id}
|
key={id}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
|
@ -56,7 +56,7 @@ export const getAlertsTypeTableColumns = (
|
||||||
<EuiHealth color={ALERT_TYPE_COLOR[type as AlertType]}>
|
<EuiHealth color={ALERT_TYPE_COLOR[type as AlertType]}>
|
||||||
<EuiText grow={false} size="xs">
|
<EuiText grow={false} size="xs">
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={4}
|
visibleCellActions={4}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const getHostRiskScoreColumns = ({
|
||||||
if (hostName != null && hostName.length > 0) {
|
if (hostName != null && hostName.length > 0) {
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const getHostsColumns = (
|
||||||
if (hostName != null && hostName.length > 0) {
|
if (hostName != null && hostName.length > 0) {
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
@ -96,7 +96,7 @@ export const getHostsColumns = (
|
||||||
if (hostOsName != null) {
|
if (hostOsName != null) {
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
@ -123,7 +123,7 @@ export const getHostsColumns = (
|
||||||
if (hostOsVersion != null) {
|
if (hostOsVersion != null) {
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export const getNetworkDnsColumns = (): NetworkDnsColumns => [
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
key={escapeDataProviderId(`networkDns-table--name-${dnsName}`)}
|
key={escapeDataProviderId(`networkDns-table--name-${dnsName}`)}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
|
@ -60,7 +60,7 @@ export const getNetworkTopCountriesColumns = (
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
key={id}
|
key={id}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
|
@ -68,7 +68,7 @@ export const getNetworkTopNFlowColumns = (
|
||||||
<>
|
<>
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
key={id}
|
key={id}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
@ -85,7 +85,7 @@ export const getNetworkTopNFlowColumns = (
|
||||||
{geo && (
|
{geo && (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
key={`${id}-${geo}`}
|
key={`${id}-${geo}`}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
|
@ -183,7 +183,7 @@ const NetworkDetailsComponent: React.FC = () => {
|
||||||
title={
|
title={
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
field={{ type: 'ip', value: ip, name: `${flowTarget}.ip` }}
|
field={{ type: 'ip', value: ip, name: `${flowTarget}.ip` }}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
>
|
>
|
||||||
|
|
|
@ -41,7 +41,7 @@ export const getUserRiskScoreColumns = ({
|
||||||
return (
|
return (
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
key={id}
|
key={id}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
|
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 { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
|
||||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||||
import { HeaderSection } from '../../../../common/components/header_section';
|
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 { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils';
|
||||||
import type { HostAlertsItem } from './use_host_alerts_items';
|
import type { HostAlertsItem } from './use_host_alerts_items';
|
||||||
import { useHostAlertsItems } from './use_host_alerts_items';
|
import { useHostAlertsItems } from './use_host_alerts_items';
|
||||||
|
import {
|
||||||
|
SecurityCellActions,
|
||||||
|
SecurityCellActionsTrigger,
|
||||||
|
} from '../../../../common/components/cell_actions';
|
||||||
|
|
||||||
interface HostAlertsTableProps {
|
interface HostAlertsTableProps {
|
||||||
signalIndexName: string | null;
|
signalIndexName: string | null;
|
||||||
|
@ -143,6 +148,19 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.ALERTS_TEXT,
|
name: i18n.ALERTS_TEXT,
|
||||||
'data-test-subj': 'hostSeverityAlertsTable-totalAlerts',
|
'data-test-subj': 'hostSeverityAlertsTable-totalAlerts',
|
||||||
render: (totalAlerts: number, { hostName }) => (
|
render: (totalAlerts: number, { 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' }],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
data-test-subj="hostSeverityAlertsTable-totalAlertsLink"
|
data-test-subj="hostSeverityAlertsTable-totalAlertsLink"
|
||||||
disabled={totalAlerts === 0}
|
disabled={totalAlerts === 0}
|
||||||
|
@ -150,6 +168,7 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
>
|
>
|
||||||
<FormattedCount count={totalAlerts} />
|
<FormattedCount count={totalAlerts} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -157,6 +176,22 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.STATUS_CRITICAL_LABEL,
|
name: i18n.STATUS_CRITICAL_LABEL,
|
||||||
render: (count: number, { hostName }) => (
|
render: (count: number, { hostName }) => (
|
||||||
<EuiHealth data-test-subj="hostSeverityAlertsTable-critical" color={SEVERITY_COLOR.critical}>
|
<EuiHealth data-test-subj="hostSeverityAlertsTable-critical" color={SEVERITY_COLOR.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' },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
data-test-subj="hostSeverityAlertsTable-criticalLink"
|
data-test-subj="hostSeverityAlertsTable-criticalLink"
|
||||||
disabled={count === 0}
|
disabled={count === 0}
|
||||||
|
@ -164,6 +199,7 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
>
|
>
|
||||||
<FormattedCount count={count} />
|
<FormattedCount count={count} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -172,9 +208,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.STATUS_HIGH_LABEL,
|
name: i18n.STATUS_HIGH_LABEL,
|
||||||
render: (count: number, { hostName }) => (
|
render: (count: number, { hostName }) => (
|
||||||
<EuiHealth data-test-subj="hostSeverityAlertsTable-high" color={SEVERITY_COLOR.high}>
|
<EuiHealth data-test-subj="hostSeverityAlertsTable-high" color={SEVERITY_COLOR.high}>
|
||||||
<EuiLink disabled={count === 0} onClick={() => handleClick({ hostName, severity: 'high' })}>
|
<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} />
|
<FormattedCount count={count} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -183,12 +239,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.STATUS_MEDIUM_LABEL,
|
name: i18n.STATUS_MEDIUM_LABEL,
|
||||||
render: (count: number, { hostName }) => (
|
render: (count: number, { hostName }) => (
|
||||||
<EuiHealth data-test-subj="hostSeverityAlertsTable-medium" color={SEVERITY_COLOR.medium}>
|
<EuiHealth data-test-subj="hostSeverityAlertsTable-medium" color={SEVERITY_COLOR.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' },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
disabled={count === 0}
|
disabled={count === 0}
|
||||||
onClick={() => handleClick({ hostName, severity: 'medium' })}
|
onClick={() => handleClick({ hostName, severity: 'medium' })}
|
||||||
>
|
>
|
||||||
<FormattedCount count={count} />
|
<FormattedCount count={count} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -197,9 +270,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.STATUS_LOW_LABEL,
|
name: i18n.STATUS_LOW_LABEL,
|
||||||
render: (count: number, { hostName }) => (
|
render: (count: number, { hostName }) => (
|
||||||
<EuiHealth data-test-subj="hostSeverityAlertsTable-low" color={SEVERITY_COLOR.low}>
|
<EuiHealth data-test-subj="hostSeverityAlertsTable-low" color={SEVERITY_COLOR.low}>
|
||||||
<EuiLink disabled={count === 0} onClick={() => handleClick({ hostName, severity: 'low' })}>
|
<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} />
|
<FormattedCount count={count} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,6 +21,8 @@ import {
|
||||||
import { FormattedRelative } from '@kbn/i18n-react';
|
import { FormattedRelative } from '@kbn/i18n-react';
|
||||||
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||||
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
|
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 { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
|
||||||
import { HeaderSection } from '../../../../common/components/header_section';
|
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 { BUTTON_CLASS as INSPECT_BUTTON_CLASS } from '../../../../common/components/inspect';
|
||||||
import { LastUpdatedAt } from '../../../../common/components/last_updated_at';
|
import { LastUpdatedAt } from '../../../../common/components/last_updated_at';
|
||||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||||
|
import { SecurityCellActions } from '../../../../common/components/cell_actions';
|
||||||
|
|
||||||
export interface RuleAlertsTableProps {
|
export interface RuleAlertsTableProps {
|
||||||
signalIndexName: string | null;
|
signalIndexName: string | null;
|
||||||
|
@ -95,6 +98,19 @@ export const getTableColumns: GetTableColumns = ({
|
||||||
name: i18n.RULE_ALERTS_COLUMN_ALERT_COUNT,
|
name: i18n.RULE_ALERTS_COLUMN_ALERT_COUNT,
|
||||||
'data-test-subj': 'severityRuleAlertsTable-alertCount',
|
'data-test-subj': 'severityRuleAlertsTable-alertCount',
|
||||||
render: (alertCount: number, { name }) => (
|
render: (alertCount: number, { 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' }],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
data-test-subj="severityRuleAlertsTable-alertCountLink"
|
data-test-subj="severityRuleAlertsTable-alertCountLink"
|
||||||
disabled={alertCount === 0}
|
disabled={alertCount === 0}
|
||||||
|
@ -102,6 +118,7 @@ export const getTableColumns: GetTableColumns = ({
|
||||||
>
|
>
|
||||||
<FormattedCount count={alertCount} />
|
<FormattedCount count={alertCount} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,6 +20,8 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
|
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 { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
|
||||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||||
import { HeaderSection } from '../../../../common/components/header_section';
|
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 { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils';
|
||||||
import type { UserAlertsItem } from './use_user_alerts_items';
|
import type { UserAlertsItem } from './use_user_alerts_items';
|
||||||
import { useUserAlertsItems } from './use_user_alerts_items';
|
import { useUserAlertsItems } from './use_user_alerts_items';
|
||||||
|
import { SecurityCellActions } from '../../../../common/components/cell_actions';
|
||||||
|
|
||||||
interface UserAlertsTableProps {
|
interface UserAlertsTableProps {
|
||||||
signalIndexName: string | null;
|
signalIndexName: string | null;
|
||||||
|
@ -142,6 +145,19 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.ALERTS_TEXT,
|
name: i18n.ALERTS_TEXT,
|
||||||
'data-test-subj': 'userSeverityAlertsTable-totalAlerts',
|
'data-test-subj': 'userSeverityAlertsTable-totalAlerts',
|
||||||
render: (totalAlerts: number, { userName }) => (
|
render: (totalAlerts: number, { 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' }],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
data-test-subj="userSeverityAlertsTable-totalAlertsLink"
|
data-test-subj="userSeverityAlertsTable-totalAlertsLink"
|
||||||
disabled={totalAlerts === 0}
|
disabled={totalAlerts === 0}
|
||||||
|
@ -149,6 +165,7 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
>
|
>
|
||||||
<FormattedCount count={totalAlerts} />
|
<FormattedCount count={totalAlerts} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -156,6 +173,22 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.STATUS_CRITICAL_LABEL,
|
name: i18n.STATUS_CRITICAL_LABEL,
|
||||||
render: (count: number, { userName }) => (
|
render: (count: number, { userName }) => (
|
||||||
<EuiHealth data-test-subj="userSeverityAlertsTable-critical" color={SEVERITY_COLOR.critical}>
|
<EuiHealth data-test-subj="userSeverityAlertsTable-critical" color={SEVERITY_COLOR.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' },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
data-test-subj="userSeverityAlertsTable-criticalLink"
|
data-test-subj="userSeverityAlertsTable-criticalLink"
|
||||||
disabled={count === 0}
|
disabled={count === 0}
|
||||||
|
@ -163,6 +196,7 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
>
|
>
|
||||||
<FormattedCount count={count} />
|
<FormattedCount count={count} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -171,9 +205,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.STATUS_HIGH_LABEL,
|
name: i18n.STATUS_HIGH_LABEL,
|
||||||
render: (count: number, { userName }) => (
|
render: (count: number, { userName }) => (
|
||||||
<EuiHealth data-test-subj="userSeverityAlertsTable-high" color={SEVERITY_COLOR.high}>
|
<EuiHealth data-test-subj="userSeverityAlertsTable-high" color={SEVERITY_COLOR.high}>
|
||||||
<EuiLink disabled={count === 0} onClick={() => handleClick({ userName, severity: 'high' })}>
|
<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} />
|
<FormattedCount count={count} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -182,12 +236,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.STATUS_MEDIUM_LABEL,
|
name: i18n.STATUS_MEDIUM_LABEL,
|
||||||
render: (count: number, { userName }) => (
|
render: (count: number, { userName }) => (
|
||||||
<EuiHealth data-test-subj="userSeverityAlertsTable-medium" color={SEVERITY_COLOR.medium}>
|
<EuiHealth data-test-subj="userSeverityAlertsTable-medium" color={SEVERITY_COLOR.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' },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
disabled={count === 0}
|
disabled={count === 0}
|
||||||
onClick={() => handleClick({ userName, severity: 'medium' })}
|
onClick={() => handleClick({ userName, severity: 'medium' })}
|
||||||
>
|
>
|
||||||
<FormattedCount count={count} />
|
<FormattedCount count={count} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -196,9 +267,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [
|
||||||
name: i18n.STATUS_LOW_LABEL,
|
name: i18n.STATUS_LOW_LABEL,
|
||||||
render: (count: number, { userName }) => (
|
render: (count: number, { userName }) => (
|
||||||
<EuiHealth data-test-subj="userSeverityAlertsTable-low" color={SEVERITY_COLOR.low}>
|
<EuiHealth data-test-subj="userSeverityAlertsTable-low" color={SEVERITY_COLOR.low}>
|
||||||
<EuiLink disabled={count === 0} onClick={() => handleClick({ userName, severity: 'low' })}>
|
<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} />
|
<FormattedCount count={count} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -140,6 +140,19 @@ export const getRiskScoreColumns = (
|
||||||
truncateText: false,
|
truncateText: false,
|
||||||
mobileOptions: { show: true },
|
mobileOptions: { show: true },
|
||||||
render: (alertCount: number, risk) => (
|
render: (alertCount: number, risk) => (
|
||||||
|
<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' }],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EuiLink
|
<EuiLink
|
||||||
data-test-subj="risk-score-alerts"
|
data-test-subj="risk-score-alerts"
|
||||||
disabled={alertCount === 0}
|
disabled={alertCount === 0}
|
||||||
|
@ -151,6 +164,7 @@ export const getRiskScoreColumns = (
|
||||||
>
|
>
|
||||||
<FormattedCount count={alertCount} />
|
<FormattedCount count={alertCount} />
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
</SecurityCellActions>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,11 +5,10 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render, fireEvent } from '@testing-library/react';
|
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestProviders } from '../../../../common/mock';
|
import { TestProviders } from '../../../../common/mock';
|
||||||
import { EntityAnalyticsRiskScores } from '.';
|
import { EntityAnalyticsRiskScores } from '.';
|
||||||
import type { UserRiskScore } from '../../../../../common/search_strategy';
|
|
||||||
import { RiskScoreEntity, RiskSeverity } from '../../../../../common/search_strategy';
|
import { RiskScoreEntity, RiskSeverity } from '../../../../../common/search_strategy';
|
||||||
import type { SeverityCount } from '../../../../explore/components/risk_score/severity/types';
|
import type { SeverityCount } from '../../../../explore/components/risk_score/severity/types';
|
||||||
import { useRiskScore, useRiskScoreKpi } from '../../../../explore/containers/risk_score';
|
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();
|
expect(queryByTestId('entity_analytics_content')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders alerts count', () => {
|
it('renders alerts count', async () => {
|
||||||
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
|
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
|
||||||
mockUseRiskScoreKpi.mockReturnValue({
|
mockUseRiskScoreKpi.mockReturnValue({
|
||||||
severityCount: mockSeverityCount,
|
severityCount: mockSeverityCount,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
const alertsCount = 999;
|
const alertsCount = 999;
|
||||||
const data: UserRiskScore[] = [
|
const data = [
|
||||||
{
|
{
|
||||||
'@timestamp': '1234567899',
|
'@timestamp': '1234567899',
|
||||||
user: {
|
[riskEntity]: {
|
||||||
name: 'testUsermame',
|
name: 'testUsermame',
|
||||||
risk: {
|
risk: {
|
||||||
rule_risks: [],
|
rule_risks: [],
|
||||||
|
@ -176,10 +175,12 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString());
|
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() });
|
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
|
||||||
mockUseRiskScoreKpi.mockReturnValue({
|
mockUseRiskScoreKpi.mockReturnValue({
|
||||||
severityCount: mockSeverityCount,
|
severityCount: mockSeverityCount,
|
||||||
|
@ -211,6 +212,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
||||||
|
|
||||||
fireEvent.click(getByTestId('risk-score-alerts'));
|
fireEvent.click(getByTestId('risk-score-alerts'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([
|
expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([
|
||||||
{
|
{
|
||||||
title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User',
|
title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User',
|
||||||
|
@ -219,5 +221,6 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -317,7 +317,7 @@ export const MoreContainer = React.memo<MoreContainerProps>(
|
||||||
<EuiFlexItem key={id}>
|
<EuiFlexItem key={id}>
|
||||||
<SecurityCellActions
|
<SecurityCellActions
|
||||||
key={id}
|
key={id}
|
||||||
mode={CellActionsMode.HOVER}
|
mode={CellActionsMode.HOVER_DOWN}
|
||||||
visibleCellActions={5}
|
visibleCellActions={5}
|
||||||
showActionTooltips
|
showActionTooltips
|
||||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue