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';