From b222e8817ed59737cfafdc554a72f994e9547eb5 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Fri, 2 May 2025 21:57:41 +0200 Subject: [PATCH] [AI4DSOC][Security Solution] Extract takeActions logic outside the detections grouping alerts table (#219878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR continues the effort started in [this previous PR](https://github.com/elastic/kibana/pull/216572). The AI4DSOC effort revealed a limitation with the current GroupedAlertsTable: it currently always displays the `Take actions` button at each group level, and the available actions are - Mark as opened - Mark as acknowledged - Marck as closed In AI4DSOC though those actions are not available. While it would have been easy and simple to just disable the actions somehow internally to the GroupedAlertsTable, this is not the correct approach. Like done in the prior PR mentioned above, the approach here consists of making this an opt-in prop to the component. This means that we now have a new `groupTakeActionItems` prop that developers have to provide if they want the `Take actions` button to be displayed. This `groupTakeActionItems` will return n array of `EuiContextMenuItem` components that will be rendered in the menu **_The 3 places where this `Take actions` exist today have been updated accordingly, to ensure to change in the logic or UI:_** - alerts table - rule details page - entity analytis risk score https://github.com/user-attachments/assets/24ff489d-ca66-457d-bd03-f09a04f67d2a https://github.com/user-attachments/assets/9324029d-f653-42bd-bca1-73a25d46c476 **_The Alert summary page visible in AI4DSOC (`searchAiLake` tier) no longer displays the `Take actions` button._** ![Screenshot 2025-05-01 at 3 14 28 PM](https://github.com/user-attachments/assets/c8b74731-8685-483d-aff3-237df8c66823) ### Notes Some code documentation and very minor cleanup was also performed. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Relates to https://github.com/elastic/security-team/issues/11973 (cherry picked from commit 22fbe0087da0e7aaf7284e413db3aa9bee387aea) # Conflicts: # x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx # x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx # x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx # x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/top_risk_score_contributors_alerts/index.tsx --- .../pages/rule_details/index.tsx | 10 +- .../alerts_table/alerts_grouping.test.tsx | 2 - .../alerts_table/alerts_grouping.tsx | 11 +- .../alerts_table/alerts_sub_grouping.tsx | 37 +++--- .../alerts_table/grouping_settings/index.tsx | 1 - .../components/alerts_table/types.ts | 37 +++--- .../use_group_take_action_items.test.tsx} | 6 +- .../use_group_take_action_items.tsx} | 117 ++++++++--------- .../pages/alerts/detection_engine.test.tsx | 121 +----------------- .../pages/alerts/detection_engine.tsx | 10 +- .../index.tsx | 8 +- 11 files changed, 125 insertions(+), 235 deletions(-) rename x-pack/solutions/security/plugins/security_solution/public/detections/{components/alerts_table/grouping_settings/group_take_action_items.test.tsx => hooks/alerts_table/use_group_take_action_items.test.tsx} (95%) rename x-pack/solutions/security/plugins/security_solution/public/detections/{components/alerts_table/grouping_settings/group_take_action_items.tsx => hooks/alerts_table/use_group_take_action_items.tsx} (78%) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index e7e46a09b2cf4..096ada86f33f9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -39,6 +39,7 @@ import { TableId, } from '@kbn/securitysolution-data-table'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; +import { useGroupTakeActionsItems } from '../../../../detections/hooks/alerts_table/use_group_take_action_items'; import { defaultGroupStatsAggregations, defaultGroupStatsRenderer, @@ -559,6 +560,11 @@ const RuleDetailsPageComponent: React.FC = ({ confirmManualRuleRun, } = useManualRuleRunConfirmation(); + const groupTakeActionItems = useGroupTakeActionsItems({ + currentStatus: currentAlertStatusFilterValue, + showAlertStatusActions: Boolean(hasIndexWrite) && Boolean(hasIndexMaintenance), + }); + const accordionExtraActionGroupStats = useMemo( () => ({ aggregations: defaultGroupStatsAggregations, @@ -796,14 +802,12 @@ const RuleDetailsPageComponent: React.FC = ({ ; }; - currentAlertStatusFilterValue?: Status[]; defaultFilters?: Filter[]; /** * Default values to display in the group selection dropdown. @@ -66,8 +65,11 @@ export interface AlertsTableComponentProps { from: string; globalFilters: Filter[]; globalQuery: Query; - hasIndexMaintenance: boolean; - hasIndexWrite: boolean; + /** + * Allows to customize the content of the Take actions button rendered at the group level. + * If no value is provided, the Take actins button is not displayed. + */ + groupTakeActionItems?: GroupTakeActionItems; loading: boolean; renderChildComponent: (groupingFilters: Filter[]) => React.ReactElement; runtimeMappings: RunTimeMappings; @@ -326,6 +328,7 @@ const GroupedAlertsTableComponent: React.FC = (props) getGrouping={getGrouping} groupingLevel={level} groupStatsAggregations={groupStatusAggregations} + groupTakeActionItems={props.groupTakeActionItems} onGroupClose={() => resetGroupChildrenPagination(level)} pageIndex={pageIndex[level] ?? DEFAULT_PAGE_INDEX} pageSize={pageSize[level] ?? DEFAULT_PAGE_SIZE} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx index 1c37dbef28572..90aa4630cb18d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx @@ -15,11 +15,11 @@ import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { DynamicGroupingProps } from '@kbn/grouping/src'; import { parseGroupingQuery } from '@kbn/grouping/src'; import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; +import type { GroupTakeActionItems } from './types'; import type { RunTimeMappings } from '../../../sourcerer/store/model'; import { SourcererScopeName } from '../../../sourcerer/store/model'; import { combineQueries } from '../../../common/lib/kuery'; import type { AlertsGroupingAggregation } from './grouping_settings/types'; -import type { Status } from '../../../../common/api/detection_engine'; import { InspectButton } from '../../../common/components/inspect'; import { useSourcererDataView } from '../../../sourcerer/containers'; import { useKibana } from '../../../common/lib/kibana'; @@ -31,13 +31,12 @@ import { buildTimeRangeFilter } from './helpers'; import * as i18n from './translations'; import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; -import { getAlertsGroupingQuery, useGroupTakeActionsItems } from './grouping_settings'; +import { getAlertsGroupingQuery } from './grouping_settings'; const ALERTS_GROUPING_ID = 'alerts-grouping'; const DEFAULT_FILTERS: Filter[] = []; interface OwnProps { - currentAlertStatusFilterValue?: Status[]; defaultFilters?: Filter[]; from: string; getGrouping: ( @@ -51,8 +50,11 @@ interface OwnProps { * This is then used to render values in the EuiAccordion `extraAction` section. */ groupStatsAggregations: (field: string) => NamedAggregation[]; - hasIndexMaintenance: boolean; - hasIndexWrite: boolean; + /** + * Allows to customize the content of the Take actions button rendered at the group level. + * If no value is provided, the Take actins button is not displayed. + */ + groupTakeActionItems?: GroupTakeActionItems; loading: boolean; onGroupClose: () => void; pageIndex: number; @@ -71,7 +73,6 @@ interface OwnProps { export type AlertsTableComponentProps = OwnProps; export const GroupedSubLevelComponent: React.FC = ({ - currentAlertStatusFilterValue, defaultFilters = DEFAULT_FILTERS, from, getGrouping, @@ -79,8 +80,7 @@ export const GroupedSubLevelComponent: React.FC = ({ globalQuery, groupingLevel, groupStatsAggregations, - hasIndexMaintenance, - hasIndexWrite, + groupTakeActionItems, loading, onGroupClose, pageIndex, @@ -109,7 +109,7 @@ export const GroupedSubLevelComponent: React.FC = ({ indexPattern, browserFields, filters: [ - ...(defaultFilters ?? []), + ...defaultFilters, ...globalFilters, ...customFilters, ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), @@ -243,20 +243,18 @@ export const GroupedSubLevelComponent: React.FC = ({ [uniqueQueryId] ); - const takeActionItems = useGroupTakeActionsItems({ - currentStatus: currentAlertStatusFilterValue, - showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, - }); - const getTakeActionItems = useCallback( - (groupFilters: Filter[], groupNumber: number) => - takeActionItems({ + (groupFilters: Filter[], groupNumber: number) => { + const takeActionParams = { groupNumber, query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, selectedGroup, tableId, - }), - [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] + }; + + return groupTakeActionItems?.(takeActionParams) ?? []; + }, + [defaultFilters, getGlobalQuery, groupTakeActionItems, selectedGroup, tableId] ); const onChangeGroupsItemsPerPage = useCallback( @@ -280,13 +278,14 @@ export const GroupedSubLevelComponent: React.FC = ({ onGroupClose, renderChildComponent, selectedGroup, - takeActionItems: getTakeActionItems, + ...(groupTakeActionItems && { takeActionItems: getTakeActionItems }), }), [ aggs, getGrouping, getTakeActionItems, groupingLevel, + groupTakeActionItems, inspect, isLoadingGroups, loading, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/index.tsx index 83b03e1105cdb..a0b15a1bbf7b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/index.tsx @@ -9,5 +9,4 @@ export * from './default_grouping_options'; export * from './default_group_stats_aggregations'; export * from './default_group_stats_renderers'; export * from './default_group_title_renderers'; -export * from './group_take_action_items'; export * from './query_builder'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/types.ts index ee75ba69cc747..15c810a9fabc2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -17,7 +17,6 @@ import type { Status } from '../../../../common/api/detection_engine'; import type { Note } from '../../../../common/api/timeline'; import type { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import type { TimelineModel } from '../../../timelines/store/model'; -import type { inputsModel } from '../../../common/store'; import type { ControlColumnProps, RowRenderer } from '../../../../common/types'; export interface SetEventsLoadingProps { @@ -30,23 +29,6 @@ export interface SetEventsDeletedProps { isDeleted: boolean; } -export interface UpdateAlertsStatusProps { - alertIds: string[]; - status: Status; - selectedStatus: Status; -} - -export type UpdateAlertsStatusCallback = ( - refetchQuery: inputsModel.Refetch, - { alertIds, status, selectedStatus }: UpdateAlertsStatusProps -) => void; - -export type UpdateAlertsStatus = ({ - alertIds, - status, - selectedStatus, -}: UpdateAlertsStatusProps) => void; - export interface UpdateAlertStatusActionProps { query?: string; alertIds: string[]; @@ -99,3 +81,22 @@ export type SecurityAlertsTableProps = AlertsTablePropsWithRef = NonNullable; export type { AlertWithLegacyFormats } from '@kbn/response-ops-alerts-table/types'; + +export type GroupTakeActionItems = (props: { + /** + * Query to run when an item is clicked (meaning when an alert status is updated) + */ + query?: string; + /** + * Id of the table (used for telemetry) + */ + tableId: string; + /** + * Group number to know which group to apply the logic to. This value is coming from the callback in the kbn-grouping package. + */ + groupNumber: number; + /** + * Selected group to know which group is extended/visible. This is coming from the getLevel function in the detections alert grouping code. + */ + selectedGroup: string; +}) => JSX.Element[]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alerts_table/use_group_take_action_items.test.tsx similarity index 95% rename from x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alerts_table/use_group_take_action_items.test.tsx index 7e1c9e0a1cfbb..0c369dbdec2ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alerts_table/use_group_take_action_items.test.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import { waitFor, renderHook } from '@testing-library/react'; +import { renderHook, waitFor } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../../../common/mock'; -import { useGroupTakeActionsItems } from '.'; +import { TestProviders } from '../../../common/mock'; +import { useGroupTakeActionsItems } from './use_group_take_action_items'; describe('useGroupTakeActionsItems', () => { const wrapperContainer: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alerts_table/use_group_take_action_items.tsx similarity index 78% rename from x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alerts_table/use_group_take_action_items.tsx index e5e753b1c7763..b306bf8ff6613 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alerts_table/use_group_take_action_items.tsx @@ -8,33 +8,29 @@ import React, { useCallback, useMemo } from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { Status } from '../../../../../common/api/detection_engine'; -import type { inputsModel } from '../../../../common/store'; -import { inputsSelectors } from '../../../../common/store'; -import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import type { AlertWorkflowStatus } from '../../../../common/types'; -import { APM_USER_INTERACTIONS } from '../../../../common/lib/apm/constants'; -import { updateAlertStatus } from '../../../../common/components/toolbar/bulk_actions/update_alerts'; +import type { GroupTakeActionItems } from '../../components/alerts_table/types'; +import type { Status } from '../../../../common/api/detection_engine'; +import type { inputsModel } from '../../../common/store'; +import { inputsSelectors } from '../../../common/store'; +import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import type { AlertWorkflowStatus } from '../../../common/types'; +import { APM_USER_INTERACTIONS } from '../../../common/lib/apm/constants'; +import { updateAlertStatus } from '../../../common/components/toolbar/bulk_actions/update_alerts'; import { BULK_ACTION_ACKNOWLEDGED_SELECTED, BULK_ACTION_CLOSE_SELECTED, BULK_ACTION_OPEN_SELECTED, -} from '../../../../common/components/toolbar/bulk_actions/translations'; +} from '../../../common/components/toolbar/bulk_actions/translations'; import { UPDATE_ALERT_STATUS_FAILED, UPDATE_ALERT_STATUS_FAILED_DETAILED, -} from '../../../../common/translations'; -import { FILTER_ACKNOWLEDGED, FILTER_CLOSED, FILTER_OPEN } from '../../../../../common/types'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +} from '../../../common/translations'; +import { FILTER_ACKNOWLEDGED, FILTER_CLOSED, FILTER_OPEN } from '../../../../common/types'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import * as i18n from '../translations'; -import { AlertsEventTypes, METRIC_TYPE, track } from '../../../../common/lib/telemetry'; -import type { StartServices } from '../../../../types'; - -export interface TakeActionsProps { - currentStatus?: Status[]; - showAlertStatusActions?: boolean; -} +import { AlertsEventTypes, METRIC_TYPE, track } from '../../../common/lib/telemetry'; +import type { StartServices } from '../../../types'; const getTelemetryEvent = { groupedAlertsTakeAction: ({ @@ -48,21 +44,38 @@ const getTelemetryEvent = { }) => `alerts_table_${tableId}_group-${groupNumber}_mark-${status}`, }; +export interface UseGroupTakeActionsItemsParams { + /** + * Optional property to allow filtering the options in the dropdown depending on the current alert statuses + */ + currentStatus?: Status[]; + /** + * If true, will show all the action items + */ + showAlertStatusActions?: boolean; +} + +/** + * Hook returning a set of action items to be accessed when users click on the Take actions button displayed at the grouping alerts table group level. + * Currently the action returned are: mark as opened, mark as acknowledged or mark as closed. + * The hook is used in the alerts page, the rule details page and the entity analytics top risk score section. + */ export const useGroupTakeActionsItems = ({ currentStatus, showAlertStatusActions = true, -}: TakeActionsProps) => { +}: UseGroupTakeActionsItemsParams): GroupTakeActionItems => { const { addSuccess, addError, addWarning } = useAppToasts(); const { startTransaction } = useStartTransaction(); const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuery(), []); const globalQueries = useDeepEqualSelector(getGlobalQuerySelector); - const refetchQuery = useCallback(() => { - globalQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); - }, [globalQueries]); const { services: { telemetry }, } = useKibana(); + const refetchQuery = useCallback(() => { + globalQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }, [globalQueries]); + const reportAlertsGroupingTakeActionClick = useCallback( (params: { tableId: string; @@ -75,20 +88,6 @@ export const useGroupTakeActionsItems = ({ [telemetry] ); - const onUpdateSuccess = useCallback( - (updated: number, conflicts: number, newStatus: AlertWorkflowStatus) => { - refetchQuery(); - }, - [refetchQuery] - ); - - const onUpdateFailure = useCallback( - (newStatus: AlertWorkflowStatus, error: Error) => { - refetchQuery(); - }, - [refetchQuery] - ); - const onAlertStatusUpdateSuccess = useCallback( (updated: number, conflicts: number, newStatus: AlertWorkflowStatus) => { if (conflicts > 0) { @@ -111,11 +110,9 @@ export const useGroupTakeActionsItems = ({ } addSuccess({ title }); } - if (onUpdateSuccess) { - onUpdateSuccess(updated, conflicts, newStatus); - } + refetchQuery(); }, - [addSuccess, addWarning, onUpdateSuccess] + [addSuccess, addWarning, refetchQuery] ); const onAlertStatusUpdateFailure = useCallback( @@ -132,11 +129,9 @@ export const useGroupTakeActionsItems = ({ title = i18n.ACKNOWLEDGED_ALERT_FAILED_TOAST; } addError(error.message, { title }); - if (onUpdateFailure) { - onUpdateFailure(newStatus, error); - } + refetchQuery(); }, - [addError, onUpdateFailure] + [addError, refetchQuery] ); const onClickUpdate = useCallback( @@ -189,20 +184,15 @@ export const useGroupTakeActionsItems = ({ ] ); - return useMemo(() => { - const getActionItems = ({ - query, - tableId, - groupNumber, - selectedGroup, - }: { - query?: string; - tableId: string; - groupNumber: number; - selectedGroup: string; - }) => { - const actionItems: JSX.Element[] = []; - if (showAlertStatusActions) { + return useMemo( + (): GroupTakeActionItems => + ({ query, tableId, groupNumber, selectedGroup }) => { + const actionItems: JSX.Element[] = []; + + if (!showAlertStatusActions) { + return actionItems; + } + if (currentStatus && currentStatus.length === 1) { const singleStatus = currentStatus[0]; if (singleStatus !== FILTER_OPEN) { @@ -288,10 +278,9 @@ export const useGroupTakeActionsItems = ({ ) ); } - } - return actionItems; - }; - return getActionItems; - }, [currentStatus, onClickUpdate, showAlertStatusActions]); + return actionItems; + }, + [currentStatus, onClickUpdate, showAlertStatusActions] + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.test.tsx index 166ecf083ff8c..fe7af88c25a88 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.test.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; -import { mockGlobalState, TestProviders, createMockStore } from '../../../common/mock'; +import { createMockStore, mockGlobalState, TestProviders } from '../../../common/mock'; import { useUserData } from '../../components/user_info'; import { useSourcererDataView } from '../../../sourcerer/containers'; import type { State } from '../../../common/store'; @@ -22,10 +22,8 @@ import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub import { useListsConfig } from '../../containers/detection_engine/lists/use_lists_config'; import * as alertFilterControlsPackage from '@kbn/alerts-ui-shared/src/alert_filter_controls/alert_filter_controls'; import { DetectionEnginePage } from './detection_engine'; -import type { AlertsTableComponentProps } from '../../components/alerts_table/alerts_grouping'; import { TableId } from '@kbn/securitysolution-data-table'; import { useUpsellingMessage } from '../../../common/hooks/use_upselling'; -import { mockAlertFilterControls } from '@kbn/alerts-ui-shared/src/alert_filter_controls/mocks'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -39,23 +37,9 @@ jest.mock('../../../common/hooks/use_space_id', () => ({ useSpaceId: () => 'default', })); jest.mock('@kbn/alerts-ui-shared/src/alert_filter_controls/alert_filter_controls'); - -const mockStatusCapture = jest.fn(); -const GroupedAlertsTable: React.FC = ({ - currentAlertStatusFilterValue, -}) => { - useEffect(() => { - if (currentAlertStatusFilterValue) { - mockStatusCapture(currentAlertStatusFilterValue); - } - }, [currentAlertStatusFilterValue]); - return ; -}; - jest.mock('../../components/alerts_table/alerts_grouping', () => ({ - GroupedAlertsTable, + GroupedAlertsTable: () => , })); - jest.mock('../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../components/user_info'); jest.mock('../../../sourcerer/containers'); @@ -220,9 +204,11 @@ describe('DetectionEnginePageComponent', () => { .mockImplementation(() => ); (useUpsellingMessage as jest.Mock).mockReturnValue('Go for Platinum!'); }); + beforeEach(() => { jest.clearAllMocks(); }); + it('renders correctly', async () => { const { getByTestId } = render( @@ -319,101 +305,4 @@ describe('DetectionEnginePageComponent', () => { expect.anything() ); }); - - it('the pageFiltersUpdateHandler updates status when a multi status filter is passed', async () => { - jest.spyOn(alertFilterControlsPackage, 'AlertFilterControls').mockImplementationOnce( - mockAlertFilterControls([ - { - meta: { - index: 'security-solution-default', - key: 'kibana.alert.workflow_status', - params: ['open', 'acknowledged'], - }, - }, - ]) - ); - - render( - - - - - - ); - // when statusFilter updates, we call mockStatusCapture in test mocks - expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); - expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open', 'acknowledged']); - }); - - it('the pageFiltersUpdateHandler updates status when a single status filter is passed', async () => { - jest.spyOn(alertFilterControlsPackage, 'AlertFilterControls').mockImplementation( - mockAlertFilterControls([ - { - meta: { - index: 'security-solution-default', - key: 'kibana.alert.workflow_status', - disabled: false, - }, - query: { - match_phrase: { - 'kibana.alert.workflow_status': 'open', - }, - }, - }, - { - meta: { - index: 'security-solution-default', - key: 'kibana.alert.severity', - disabled: false, - }, - query: { - match_phrase: { - 'kibana.alert.severity': 'low', - }, - }, - }, - ]) - ); - - render( - - - - - - ); - // when statusFilter updates, we call mockStatusCapture in test mocks - expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); - expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open']); - }); - - it('the pageFiltersUpdateHandler clears status when no status filter is passed', async () => { - jest.spyOn(alertFilterControlsPackage, 'AlertFilterControls').mockImplementation( - mockAlertFilterControls([ - { - meta: { - index: 'security-solution-default', - key: 'kibana.alert.severity', - disabled: false, - }, - query: { - match_phrase: { - 'kibana.alert.severity': 'low', - }, - }, - }, - ]) - ); - - render( - - - - - - ); - // when statusFilter updates, we call mockStatusCapture in test mocks - expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); - expect(mockStatusCapture).toHaveBeenNthCalledWith(2, []); - }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.tsx index 9d658a1606853..303855f9b0356 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.tsx @@ -34,6 +34,7 @@ import { import { isEqual } from 'lodash'; import type { FilterGroupHandler } from '@kbn/alerts-ui-shared'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; +import { useGroupTakeActionsItems } from '../../hooks/alerts_table/use_group_take_action_items'; import { defaultGroupingOptions, defaultGroupStatsAggregations, @@ -329,6 +330,11 @@ const DetectionEnginePageComponent: React.FC = () [alertsTableDefaultFilters, isAlertTableLoading] ); + const groupTakeActionItems = useGroupTakeActionsItems({ + currentStatus: statusFilter, + showAlertStatusActions: Boolean(hasIndexWrite) && Boolean(hasIndexMaintenance), + }); + const accordionExtraActionGroupStats = useMemo( () => ({ aggregations: defaultGroupStatsAggregations, @@ -438,14 +444,12 @@ const DetectionEnginePageComponent: React.FC = () ({ const defaultFilters = useMemo(() => [...inputFilters, ...filters], [filters, inputFilters]); + const groupTakeActionItems = useGroupTakeActionsItems({ + showAlertStatusActions: Boolean(hasIndexWrite) && Boolean(hasIndexMaintenance), + }); + const accordionExtraActionGroupStats = useMemo( () => ({ aggregations: defaultGroupStatsAggregations, @@ -138,8 +143,7 @@ export const TopRiskScoreContributorsAlerts = ({ from={from} globalFilters={filters} globalQuery={query} - hasIndexMaintenance={hasIndexMaintenance ?? false} - hasIndexWrite={hasIndexWrite ?? false} + groupTakeActionItems={groupTakeActionItems} loading={userInfoLoading || loading} renderChildComponent={renderGroupedAlertTable} runtimeMappings={sourcererDataView?.runtimeFieldMap as RunTimeMappings}