diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.test.tsx
index f7ff7a2612c5f..3dac7381cb395 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.test.tsx
@@ -8,51 +8,43 @@
import React from 'react';
import { render } from '@testing-library/react';
import type { Alert } from '@kbn/alerting-types';
-import { ActionsCell, ROW_ACTION_FLYOUT_ICON_TEST_ID } from './actions_cell';
+import { ActionsCell } from './actions_cell';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
-import { IOCPanelKey } from '../../../../flyout/ai_for_soc/constants/panel_keys';
+import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
+import { MORE_ACTIONS_BUTTON_TEST_ID } from './more_actions_row_control_column';
+import { useAddToCaseActions } from '../../alerts_table/timeline_actions/use_add_to_case_actions';
+import { useAlertTagsActions } from '../../alerts_table/timeline_actions/use_alert_tags_actions';
+import { ROW_ACTION_FLYOUT_ICON_TEST_ID } from './open_flyout_row_control_column';
jest.mock('@kbn/expandable-flyout');
+jest.mock('../../alerts_table/timeline_actions/use_add_to_case_actions');
+jest.mock('../../alerts_table/timeline_actions/use_alert_tags_actions');
describe('ActionsCell', () => {
it('should render icons', () => {
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({
openFlyout: jest.fn(),
});
+ (useAddToCaseActions as jest.Mock).mockReturnValue({
+ addToCaseActionItems: [],
+ });
+ (useAlertTagsActions as jest.Mock).mockReturnValue({
+ alertTagsItems: [],
+ alertTagsPanels: [],
+ });
const alert: Alert = {
_id: '_id',
_index: '_index',
};
-
- const { getByTestId } = render();
-
- expect(getByTestId(ROW_ACTION_FLYOUT_ICON_TEST_ID)).toBeInTheDocument();
- });
-
- it('should open flyout after click', () => {
- const openFlyout = jest.fn();
- (useExpandableFlyoutApi as jest.Mock).mockReturnValue({
- openFlyout,
- });
-
- const alert: Alert = {
+ const ecsAlert: Ecs = {
_id: '_id',
_index: '_index',
};
- const { getByTestId } = render();
+ const { getByTestId } = render();
- getByTestId(ROW_ACTION_FLYOUT_ICON_TEST_ID).click();
-
- expect(openFlyout).toHaveBeenCalledWith({
- right: {
- id: IOCPanelKey,
- params: {
- id: alert._id,
- indexName: alert._index,
- },
- },
- });
+ expect(getByTestId(ROW_ACTION_FLYOUT_ICON_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(MORE_ACTIONS_BUTTON_TEST_ID)).toBeInTheDocument();
});
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.tsx
index 61cf6a8cd1edd..0d4612b722a23 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.tsx
@@ -5,20 +5,22 @@
* 2.0.
*/
-import React, { memo, useCallback } from 'react';
-import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
-import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import React, { memo } from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { Alert } from '@kbn/alerting-types';
-import { i18n } from '@kbn/i18n';
-import { IOCPanelKey } from '../../../../flyout/ai_for_soc/constants/panel_keys';
-
-export const ROW_ACTION_FLYOUT_ICON_TEST_ID = 'alert-summary-table-row-action-flyout-icon';
+import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
+import { OpenFlyoutRowControlColumn } from './open_flyout_row_control_column';
+import { MoreActionsRowControlColumn } from './more_actions_row_control_column';
export interface ActionsCellProps {
/**
* Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface
*/
alert: Alert;
+ /**
+ * The Ycs type is @deprecated but needed for the case actions within the more action dropdown
+ */
+ ecsAlert: Ecs;
}
/**
@@ -29,38 +31,15 @@ export interface ActionsCellProps {
* - assistant (soon)
* - more actions (soon)
*/
-export const ActionsCell = memo(({ alert }: ActionsCellProps) => {
- const { openFlyout } = useExpandableFlyoutApi();
- const onOpenFlyout = useCallback(
- () =>
- openFlyout({
- right: {
- id: IOCPanelKey,
- params: {
- id: alert._id,
- indexName: alert._index,
- },
- },
- }),
- [alert, openFlyout]
- );
-
- return (
-
-
-
-
-
- );
-});
+export const ActionsCell = memo(({ alert, ecsAlert }: ActionsCellProps) => (
+
+
+
+
+
+
+
+
+));
ActionsCell.displayName = 'ActionsCell';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/more_actions_row_control_column.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/more_actions_row_control_column.test.tsx
new file mode 100644
index 0000000000000..96dfafcbdbe2f
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/more_actions_row_control_column.test.tsx
@@ -0,0 +1,131 @@
+/*
+ * 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 React from 'react';
+import { render } from '@testing-library/react';
+import {
+ MORE_ACTIONS_BUTTON_TEST_ID,
+ MoreActionsRowControlColumn,
+} from './more_actions_row_control_column';
+import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
+import { useKibana } from '../../../../common/lib/kibana';
+import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
+import { useAlertsPrivileges } from '../../../containers/detection_engine/alerts/use_alerts_privileges';
+
+jest.mock('../../../../common/lib/kibana');
+jest.mock('../../../containers/detection_engine/alerts/use_alerts_privileges');
+
+describe('MoreActionsRowControlColumn', () => {
+ it('should render component with all options', () => {
+ (useAlertsPrivileges as jest.Mock).mockReturnValue({ hasIndexWrite: true });
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ cases: {
+ ...mockCasesContract(),
+ helpers: {
+ canUseCases: jest.fn().mockReturnValue({
+ read: true,
+ createComment: true,
+ }),
+ getRuleIdFromEvent: jest.fn(),
+ },
+ },
+ },
+ });
+
+ const ecsAlert: Ecs = {
+ _id: '_id',
+ _index: '_index',
+ event: { kind: ['signal'] },
+ kibana: { alert: { workflow_tags: [] } },
+ };
+
+ const { getByTestId } = render();
+
+ const button = getByTestId(MORE_ACTIONS_BUTTON_TEST_ID);
+ expect(button).toBeInTheDocument();
+
+ button.click();
+
+ expect(getByTestId('add-to-existing-case-action')).toBeInTheDocument();
+ expect(getByTestId('add-to-new-case-action')).toBeInTheDocument();
+ expect(getByTestId('alert-tags-context-menu-item')).toBeInTheDocument();
+ });
+
+ it('should not show cases actions if user is not authorized', () => {
+ (useAlertsPrivileges as jest.Mock).mockReturnValue({ hasIndexWrite: true });
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ cases: {
+ ...mockCasesContract(),
+ helpers: {
+ canUseCases: jest.fn().mockReturnValue({
+ read: false,
+ createComment: false,
+ }),
+ getRuleIdFromEvent: jest.fn(),
+ },
+ },
+ },
+ });
+
+ const ecsAlert: Ecs = {
+ _id: '_id',
+ _index: '_index',
+ event: { kind: ['signal'] },
+ kibana: { alert: { workflow_tags: [] } },
+ };
+
+ const { getByTestId, queryByTestId } = render(
+
+ );
+
+ const button = getByTestId(MORE_ACTIONS_BUTTON_TEST_ID);
+ expect(button).toBeInTheDocument();
+
+ button.click();
+
+ expect(queryByTestId('add-to-existing-case-action')).not.toBeInTheDocument();
+ expect(queryByTestId('add-to-new-case-action')).not.toBeInTheDocument();
+ });
+
+ it('should not show tags actions if user is not authorized', () => {
+ (useAlertsPrivileges as jest.Mock).mockReturnValue({ hasIndexWrite: false });
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ cases: {
+ ...mockCasesContract(),
+ helpers: {
+ canUseCases: jest.fn().mockReturnValue({
+ read: true,
+ createComment: true,
+ }),
+ getRuleIdFromEvent: jest.fn(),
+ },
+ },
+ },
+ });
+
+ const ecsAlert: Ecs = {
+ _id: '_id',
+ _index: '_index',
+ event: { kind: ['signal'] },
+ kibana: { alert: { workflow_tags: [] } },
+ };
+
+ const { getByTestId, queryByTestId } = render(
+
+ );
+
+ const button = getByTestId(MORE_ACTIONS_BUTTON_TEST_ID);
+ expect(button).toBeInTheDocument();
+
+ button.click();
+
+ expect(queryByTestId('alert-tags-context-menu-item')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/more_actions_row_control_column.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/more_actions_row_control_column.tsx
new file mode 100644
index 0000000000000..3075efbc8b338
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/more_actions_row_control_column.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 React, { memo, useCallback, useMemo, useState } from 'react';
+import { EuiButtonIcon, EuiContextMenu, EuiPopover } from '@elastic/eui';
+import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
+import { i18n } from '@kbn/i18n';
+import { useAlertTagsActions } from '../../alerts_table/timeline_actions/use_alert_tags_actions';
+import { useAddToCaseActions } from '../../alerts_table/timeline_actions/use_add_to_case_actions';
+
+export const MORE_ACTIONS_BUTTON_TEST_ID = 'alert-summary-table-row-action-more-actions';
+
+export const MORE_ACTIONS_BUTTON_ARIA_LABEL = i18n.translate(
+ 'xpack.securitySolution.alertSummary.table.moreActionsAriaLabel',
+ {
+ defaultMessage: 'More actions',
+ }
+);
+export const ADD_TO_CASE_ARIA_LABEL = i18n.translate(
+ 'xpack.securitySolution.alertSummary.table.attachToCaseAriaLabel',
+ {
+ defaultMessage: 'Attach alert to case',
+ }
+);
+
+export interface MoreActionsRowControlColumnProps {
+ /**
+ * Alert data
+ * The Ecs type is @deprecated but needed for the case actions within the more action dropdown
+ */
+ ecsAlert: Ecs;
+}
+
+/**
+ * Renders a horizontal 3-dot button which displays a context menu when clicked.
+ * This is used in the AI for SOC alert summary table.
+ * The following options are available:
+ * - add to existing case
+ * - add to new case
+ * - apply alert tags
+ */
+export const MoreActionsRowControlColumn = memo(
+ ({ ecsAlert }: MoreActionsRowControlColumnProps) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const togglePopover = useCallback(() => setIsPopoverOpen((value) => !value), []);
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+
+ const button = useMemo(
+ () => (
+
+ ),
+ [togglePopover]
+ );
+
+ const { addToCaseActionItems } = useAddToCaseActions({
+ ecsData: ecsAlert,
+ onMenuItemClick: closePopover,
+ isActiveTimelines: false,
+ ariaLabel: ADD_TO_CASE_ARIA_LABEL,
+ isInDetections: true,
+ });
+
+ const { alertTagsItems, alertTagsPanels } = useAlertTagsActions({
+ closePopover,
+ ecsRowData: ecsAlert,
+ });
+
+ const panels = useMemo(
+ () => [
+ {
+ id: 0,
+ items: [...addToCaseActionItems, ...alertTagsItems],
+ },
+ ...alertTagsPanels,
+ ],
+ [addToCaseActionItems, alertTagsItems, alertTagsPanels]
+ );
+
+ return (
+
+
+
+ );
+ }
+);
+
+MoreActionsRowControlColumn.displayName = 'MoreActionsRowControlColumn';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/open_flyout_row_control_column.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/open_flyout_row_control_column.test.tsx
new file mode 100644
index 0000000000000..59c4e53851a72
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/open_flyout_row_control_column.test.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 React from 'react';
+import { render } from '@testing-library/react';
+import type { Alert } from '@kbn/alerting-types';
+import {
+ OpenFlyoutRowControlColumn,
+ ROW_ACTION_FLYOUT_ICON_TEST_ID,
+} from './open_flyout_row_control_column';
+import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { IOCPanelKey } from '../../../../flyout/ai_for_soc/constants/panel_keys';
+
+jest.mock('@kbn/expandable-flyout');
+
+describe('OpenFlyoutRowControlColumn', () => {
+ it('should render button icon', () => {
+ (useExpandableFlyoutApi as jest.Mock).mockReturnValue({
+ openFlyout: jest.fn(),
+ });
+
+ const alert: Alert = {
+ _id: '_id',
+ _index: '_index',
+ };
+
+ const { getByTestId } = render();
+
+ expect(getByTestId(ROW_ACTION_FLYOUT_ICON_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should open flyout after click', () => {
+ const openFlyout = jest.fn();
+ (useExpandableFlyoutApi as jest.Mock).mockReturnValue({
+ openFlyout,
+ });
+
+ const alert: Alert = {
+ _id: '_id',
+ _index: '_index',
+ };
+
+ const { getByTestId } = render();
+
+ getByTestId(ROW_ACTION_FLYOUT_ICON_TEST_ID).click();
+
+ expect(openFlyout).toHaveBeenCalledWith({
+ right: {
+ id: IOCPanelKey,
+ params: {
+ id: alert._id,
+ indexName: alert._index,
+ },
+ },
+ });
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/open_flyout_row_control_column.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/open_flyout_row_control_column.tsx
new file mode 100644
index 0000000000000..dcfc0fc7ebfa5
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/open_flyout_row_control_column.tsx
@@ -0,0 +1,57 @@
+/*
+ * 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 React, { memo, useCallback } from 'react';
+import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { EuiButtonIcon } from '@elastic/eui';
+import type { Alert } from '@kbn/alerting-types';
+import { i18n } from '@kbn/i18n';
+import { IOCPanelKey } from '../../../../flyout/ai_for_soc/constants/panel_keys';
+
+export const ROW_ACTION_FLYOUT_ICON_TEST_ID = 'alert-summary-table-row-action-flyout-icon';
+
+export interface ActionsCellProps {
+ /**
+ * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface
+ */
+ alert: Alert;
+}
+
+/**
+ * Renders a icon to open the AI for SOC alert summary flyout.
+ */
+export const OpenFlyoutRowControlColumn = memo(({ alert }: ActionsCellProps) => {
+ const { openFlyout } = useExpandableFlyoutApi();
+ const onOpenFlyout = useCallback(
+ () =>
+ openFlyout({
+ right: {
+ id: IOCPanelKey,
+ params: {
+ id: alert._id,
+ indexName: alert._index,
+ },
+ },
+ }),
+ [alert, openFlyout]
+ );
+
+ return (
+
+ );
+});
+
+OpenFlyoutRowControlColumn.displayName = 'OpenFlyoutRowControlColumn';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
index 0efdd73bed0e2..bba449d9b140f 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
@@ -72,7 +72,7 @@ const columns: EuiDataGridProps['columns'] = [
},
];
-const ACTION_COLUMN_WIDTH = 64; // px
+const ACTION_COLUMN_WIDTH = 72; // px
const ALERT_TABLE_CONSUMERS: AlertsTableProps['consumers'] = [AlertConsumers.SIEM];
const RULE_TYPE_IDS = [ESQL_RULE_TYPE_ID, QUERY_RULE_TYPE_ID];
const ROW_HEIGHTS_OPTIONS = { defaultHeight: 40 };
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/take_action_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/take_action_button.test.tsx
new file mode 100644
index 0000000000000..103742dc56864
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/take_action_button.test.tsx
@@ -0,0 +1,128 @@
+/*
+ * 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 React from 'react';
+import { render } from '@testing-library/react';
+import { useKibana } from '../../../common/lib/kibana';
+import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
+import { TAKE_ACTION_BUTTON_TEST_ID, TakeActionButton } from './take_action_button';
+import { useAlertsPrivileges } from '../../../detections/containers/detection_engine/alerts/use_alerts_privileges';
+import { useAIForSOCDetailsContext } from '../context';
+
+jest.mock('../../../common/lib/kibana');
+jest.mock('../../../detections/containers/detection_engine/alerts/use_alerts_privileges');
+jest.mock('../context');
+
+describe('TakeActionButton', () => {
+ it('should render component with all options', () => {
+ (useAlertsPrivileges as jest.Mock).mockReturnValue({ hasIndexWrite: true });
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ cases: {
+ ...mockCasesContract(),
+ helpers: {
+ canUseCases: jest.fn().mockReturnValue({
+ read: true,
+ createComment: true,
+ }),
+ getRuleIdFromEvent: jest.fn(),
+ },
+ },
+ },
+ });
+ (useAIForSOCDetailsContext as jest.Mock).mockReturnValue({
+ dataAsNestedObject: {
+ _id: '_id',
+ _index: '_index',
+ event: { kind: ['signal'] },
+ kibana: { alert: { workflow_tags: [] } },
+ },
+ });
+
+ const { getByTestId } = render();
+
+ const button = getByTestId(TAKE_ACTION_BUTTON_TEST_ID);
+ expect(button).toBeInTheDocument();
+
+ button.click();
+
+ expect(getByTestId('add-to-existing-case-action')).toBeInTheDocument();
+ expect(getByTestId('add-to-new-case-action')).toBeInTheDocument();
+ expect(getByTestId('alert-tags-context-menu-item')).toBeInTheDocument();
+ });
+
+ it('should not show cases actions if user is not authorized', () => {
+ (useAlertsPrivileges as jest.Mock).mockReturnValue({ hasIndexWrite: true });
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ cases: {
+ ...mockCasesContract(),
+ helpers: {
+ canUseCases: jest.fn().mockReturnValue({
+ read: false,
+ createComment: false,
+ }),
+ getRuleIdFromEvent: jest.fn(),
+ },
+ },
+ },
+ });
+ (useAIForSOCDetailsContext as jest.Mock).mockReturnValue({
+ dataAsNestedObject: {
+ _id: '_id',
+ _index: '_index',
+ event: { kind: ['signal'] },
+ kibana: { alert: { workflow_tags: [] } },
+ },
+ });
+
+ const { getByTestId, queryByTestId } = render();
+
+ const button = getByTestId(TAKE_ACTION_BUTTON_TEST_ID);
+ expect(button).toBeInTheDocument();
+
+ button.click();
+
+ expect(queryByTestId('add-to-existing-case-action')).not.toBeInTheDocument();
+ expect(queryByTestId('add-to-new-case-action')).not.toBeInTheDocument();
+ });
+
+ it('should not show tags actions if user is not authorized', () => {
+ (useAlertsPrivileges as jest.Mock).mockReturnValue({ hasIndexWrite: false });
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ cases: {
+ ...mockCasesContract(),
+ helpers: {
+ canUseCases: jest.fn().mockReturnValue({
+ read: true,
+ createComment: true,
+ }),
+ getRuleIdFromEvent: jest.fn(),
+ },
+ },
+ },
+ });
+ (useAIForSOCDetailsContext as jest.Mock).mockReturnValue({
+ dataAsNestedObject: {
+ _id: '_id',
+ _index: '_index',
+ event: { kind: ['signal'] },
+ kibana: { alert: { workflow_tags: [] } },
+ },
+ });
+
+ const { getByTestId, queryByTestId } = render();
+
+ const button = getByTestId(TAKE_ACTION_BUTTON_TEST_ID);
+ expect(button).toBeInTheDocument();
+
+ button.click();
+
+ expect(queryByTestId('alert-tags-context-menu-item')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/take_action_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/take_action_button.tsx
new file mode 100644
index 0000000000000..ce353ed019b8b
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/take_action_button.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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 React, { memo, useCallback, useMemo, useState } from 'react';
+import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useAIForSOCDetailsContext } from '../context';
+import { useAddToCaseActions } from '../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions';
+import { useAlertTagsActions } from '../../../detections/components/alerts_table/timeline_actions/use_alert_tags_actions';
+
+export const TAKE_ACTION_BUTTON_TEST_ID = 'alert-summary-flyout-take-action';
+
+export const TAKE_ACTION_BUTTON = i18n.translate(
+ 'xpack.securitySolution.alertSummary.flyout.takeActionsAriaLabel',
+ {
+ defaultMessage: 'Take action',
+ }
+);
+export const ADD_TO_CASE_ARIA_LABEL = i18n.translate(
+ 'xpack.securitySolution.alertSummary.flyout.attachToCaseAriaLabel',
+ {
+ defaultMessage: 'Attach alert to case',
+ }
+);
+
+/**
+ * Take action button in the panel footer.
+ * This is used in the AI for SOC alert summary page.
+ * The following options are available:
+ * - add to existing case
+ * - add to new case
+ * - apply alert tags
+ */
+export const TakeActionButton = memo(() => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const { dataAsNestedObject } = useAIForSOCDetailsContext();
+
+ const togglePopover = useCallback(() => setIsPopoverOpen((value) => !value), []);
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+
+ const button = useMemo(
+ () => (
+
+ {TAKE_ACTION_BUTTON}
+
+ ),
+ [togglePopover]
+ );
+
+ const { addToCaseActionItems } = useAddToCaseActions({
+ ecsData: dataAsNestedObject,
+ onMenuItemClick: closePopover,
+ isActiveTimelines: false,
+ ariaLabel: ADD_TO_CASE_ARIA_LABEL,
+ isInDetections: true,
+ });
+
+ const { alertTagsItems, alertTagsPanels } = useAlertTagsActions({
+ closePopover,
+ ecsRowData: dataAsNestedObject,
+ });
+
+ const panels = useMemo(
+ () => [
+ {
+ id: 0,
+ items: [...addToCaseActionItems, ...alertTagsItems],
+ },
+ ...alertTagsPanels,
+ ],
+ [addToCaseActionItems, alertTagsItems, alertTagsPanels]
+ );
+
+ return (
+
+
+
+ );
+});
+
+TakeActionButton.displayName = 'TakeActionButton';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/context.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/context.tsx
index c0765785aec0f..690c4d67668b8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/context.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/context.tsx
@@ -7,6 +7,7 @@
import React, { createContext, memo, useContext, useMemo } from 'react';
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
+import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import type { SearchHit } from '../../../common/search_strategy';
import type { GetFieldsData } from '../document_details/shared/hooks/use_get_fields_data';
import { FlyoutLoading } from '../shared/components/flyout_loading';
@@ -31,6 +32,10 @@ export interface AIForSOCDetailsContext {
* An object containing fields by type
*/
browserFields: BrowserFields;
+ /**
+ * An object with top level fields from the ECS object
+ */
+ dataAsNestedObject: Ecs;
/**
* Retrieves searchHit values for the provided field
*/
@@ -55,24 +60,39 @@ export type AIForSOCDetailsProviderProps = {
export const AIForSOCDetailsProvider = memo(
({ id, indexName, children }: AIForSOCDetailsProviderProps) => {
- const { browserFields, dataFormattedForFieldBrowser, getFieldsData, loading, searchHit } =
- useEventDetails({
- eventId: id,
- indexName,
- });
+ const {
+ browserFields,
+ dataAsNestedObject,
+ dataFormattedForFieldBrowser,
+ getFieldsData,
+ loading,
+ searchHit,
+ } = useEventDetails({
+ eventId: id,
+ indexName,
+ });
const contextValue = useMemo(
() =>
- dataFormattedForFieldBrowser && id && indexName && searchHit
+ dataFormattedForFieldBrowser && dataAsNestedObject && id && indexName && searchHit
? {
browserFields,
dataFormattedForFieldBrowser,
+ dataAsNestedObject,
eventId: id,
getFieldsData,
indexName,
searchHit,
}
: undefined,
- [browserFields, dataFormattedForFieldBrowser, getFieldsData, id, indexName, searchHit]
+ [
+ browserFields,
+ dataAsNestedObject,
+ dataFormattedForFieldBrowser,
+ getFieldsData,
+ id,
+ indexName,
+ searchHit,
+ ]
);
if (loading) {
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/footer.tsx
index fc2d60fe07bf4..c117c0b780b83 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/footer.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/footer.tsx
@@ -5,22 +5,25 @@
* 2.0.
*/
-import React from 'react';
+import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiPanel } from '@elastic/eui';
+import { TakeActionButton } from './components/take_action_button';
export const FLYOUT_FOOTER_TEST_ID = 'ai-for-soc-alert-flyout-footer';
/**
* Bottom section of the flyout that contains the take action button
*/
-export const PanelFooter = () => (
+export const PanelFooter = memo(() => (
-
+
+
+
-);
+));
PanelFooter.displayName = 'PanelFooter';