diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/components/assignees.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/components/assignees.test.tsx index 345133742c4bc..bb43a391e54a2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/components/assignees.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/components/assignees.test.tsx @@ -10,39 +10,89 @@ import { render, screen } from '@testing-library/react'; import { Assignees } from './assignees'; import { TestProviders } from '../../../common/mock'; -import { useAttackDetailsAssignees } from '../hooks/use_attack_details_assignees'; +import { useAttackDetailsContext } from '../context'; +import { useHeaderData } from '../hooks/use_header_data'; +import { useAttackAssigneesContextMenuItems } from '../../../detections/hooks/attacks/bulk_actions/context_menu_items/use_attack_assignees_context_menu_items'; +import { useInvalidateFindAttackDiscoveries } from '../../../attack_discovery/pages/use_find_attack_discoveries'; +import { useAttacksPrivileges } from '../../../detections/hooks/attacks/bulk_actions/use_attacks_privileges'; +import { useLicense } from '../../../common/hooks/use_license'; +import { useUpsellingMessage } from '../../../common/hooks/use_upselling'; import { HEADER_ASSIGNEES_ADD_BUTTON_TEST_ID } from '../constants/test_ids'; import { - USERS_AVATARS_COUNT_BADGE_TEST_ID, USERS_AVATARS_PANEL_TEST_ID, USER_AVATAR_ITEM_TEST_ID, } from '../../../common/components/user_profiles/test_ids'; -jest.mock('../hooks/use_attack_details_assignees'); +jest.mock('../context'); +jest.mock('../hooks/use_header_data'); +jest.mock( + '../../../detections/hooks/attacks/bulk_actions/context_menu_items/use_attack_assignees_context_menu_items' +); +jest.mock('../../../attack_discovery/pages/use_find_attack_discoveries'); +jest.mock('../../../detections/hooks/attacks/bulk_actions/use_attacks_privileges'); +jest.mock('../../../common/hooks/use_license'); +jest.mock('../../../common/hooks/use_upselling'); +jest.mock('../../../common/components/user_profiles/use_bulk_get_user_profiles', () => ({ + useBulkGetUserProfiles: ({ uids }: { uids: Set }) => ({ + data: + uids.size > 0 + ? [ + { + uid: 'uid-1', + enabled: true, + user: { username: 'user1', full_name: 'User 1' }, + data: {}, + }, + ] + : undefined, + }), +})); jest.mock('../../../common/components/empty_value', () => ({ getEmptyTagValue: () => '—', })); -const mockUseAttackDetailsAssignees = useAttackDetailsAssignees as jest.MockedFunction< - typeof useAttackDetailsAssignees +const mockUseAttackDetailsContext = useAttackDetailsContext as jest.MockedFunction< + typeof useAttackDetailsContext +>; +const mockUseHeaderData = useHeaderData as jest.MockedFunction; +const mockUseAttackAssigneesContextMenuItems = + useAttackAssigneesContextMenuItems as jest.MockedFunction< + typeof useAttackAssigneesContextMenuItems + >; +const mockUseInvalidateFindAttackDiscoveries = + useInvalidateFindAttackDiscoveries as jest.MockedFunction< + typeof useInvalidateFindAttackDiscoveries + >; +const mockUseAttacksPrivileges = useAttacksPrivileges as jest.MockedFunction< + typeof useAttacksPrivileges +>; +const mockUseLicense = useLicense as jest.MockedFunction; +const mockUseUpsellingMessage = useUpsellingMessage as jest.MockedFunction< + typeof useUpsellingMessage >; -const defaultHookReturn = { - assignedUserIds: ['uid-1'], - assignedUsers: [ - { - uid: 'uid-1', - enabled: true, - user: { username: 'user1', full_name: 'User 1' }, - data: {}, - }, - ], - onApplyAssignees: jest.fn().mockResolvedValue(undefined), - hasPermission: true, - isPlatinumPlus: true, - upsellingMessage: undefined, - isLoading: false, +const mockRefetch = jest.fn(); +const mockInvalidateFindAttackDiscoveries = jest.fn(); + +const defaultContext = { + attackId: 'attack-123', + refetch: mockRefetch, + indexName: 'test-index', + searchHit: { _index: 'test-index' }, + browserFields: {}, + getFieldsData: jest.fn(), + dataFormattedForFieldBrowser: [], +} as ReturnType; + +const defaultHeaderData = { + alertIds: ['alert-1', 'alert-2'], + assignees: ['uid-1'], +} as ReturnType; + +const defaultMenuItems = { + items: [{ name: 'Manage assignees', panel: 2, key: 'manage' }], + panels: [{ id: 2, title: 'Assignees', content:
}], }; const renderAssignees = () => @@ -55,90 +105,123 @@ const renderAssignees = () => describe('Assignees', () => { beforeEach(() => { jest.clearAllMocks(); - mockUseAttackDetailsAssignees.mockReturnValue( - defaultHookReturn as ReturnType + mockUseAttackDetailsContext.mockReturnValue(defaultContext); + mockUseHeaderData.mockReturnValue(defaultHeaderData); + mockUseAttackAssigneesContextMenuItems.mockReturnValue(defaultMenuItems); + mockUseInvalidateFindAttackDiscoveries.mockReturnValue(mockInvalidateFindAttackDiscoveries); + mockUseAttacksPrivileges.mockReturnValue({ + hasIndexWrite: true, + hasAttackIndexWrite: true, + loading: false, + }); + mockUseLicense.mockReturnValue({ + isPlatinumPlus: () => true, + } as ReturnType); + mockUseUpsellingMessage.mockReturnValue(undefined); + }); + + it('passes attacksWithAssignees and onSuccess to useAttackAssigneesContextMenuItems', () => { + renderAssignees(); + + expect(mockUseAttackAssigneesContextMenuItems).toHaveBeenCalledWith( + expect.objectContaining({ + attacksWithAssignees: [ + { + attackId: 'attack-123', + relatedAlertIds: ['alert-1', 'alert-2'], + assignees: ['uid-1'], + }, + ], + }) ); + const call = mockUseAttackAssigneesContextMenuItems.mock.calls[0][0]; + expect(call.onSuccess).toBeDefined(); + expect(typeof call.closePopover).toBe('function'); + }); + + it('onSuccess calls refetch and invalidateFindAttackDiscoveries', () => { + let capturedOnSuccess: (() => void) | undefined; + mockUseAttackAssigneesContextMenuItems.mockImplementation((args) => { + capturedOnSuccess = args.onSuccess; + return defaultMenuItems; + }); + + renderAssignees(); + expect(capturedOnSuccess).toBeDefined(); + + capturedOnSuccess!(); + + expect(mockRefetch).toHaveBeenCalled(); + expect(mockInvalidateFindAttackDiscoveries).toHaveBeenCalled(); }); it('renders empty state when user has no permission', () => { - mockUseAttackDetailsAssignees.mockReturnValue({ - ...defaultHookReturn, - hasPermission: false, - } as ReturnType); + mockUseAttacksPrivileges.mockReturnValue({ + hasIndexWrite: false, + hasAttackIndexWrite: true, + loading: false, + }); renderAssignees(); - expect(screen.getByTestId('attackDetailsFlyoutHeaderAssigneesEmpty')).toBeInTheDocument(); - expect(screen.getByTestId('attackDetailsFlyoutHeaderAssigneesEmpty')).toHaveTextContent('—'); - expect(screen.queryByTestId('attackDetailsFlyoutHeaderAssignees')).not.toBeInTheDocument(); + expect(screen.getByTestId('attack-details-flyout-header-assignees-empty')).toBeInTheDocument(); + expect(screen.getByTestId('attack-details-flyout-header-assignees-empty')).toHaveTextContent( + '—' + ); + expect(screen.queryByTestId('attack-details-flyout-header-assignees')).not.toBeInTheDocument(); }); it('renders empty state when not platinum plus', () => { - mockUseAttackDetailsAssignees.mockReturnValue({ - ...defaultHookReturn, - isPlatinumPlus: false, - } as ReturnType); + mockUseLicense.mockReturnValue({ + isPlatinumPlus: () => false, + } as ReturnType); renderAssignees(); - expect(screen.getByTestId('attackDetailsFlyoutHeaderAssigneesEmpty')).toBeInTheDocument(); - expect(screen.queryByTestId('attackDetailsFlyoutHeaderAssignees')).not.toBeInTheDocument(); + expect(screen.getByTestId('attack-details-flyout-header-assignees-empty')).toBeInTheDocument(); + expect(screen.queryByTestId('attack-details-flyout-header-assignees')).not.toBeInTheDocument(); }); - it('renders assignees block with add button when has permission and platinum', () => { + it('renders assignees block with add button and popover when has permission', () => { renderAssignees(); - expect(screen.getByTestId('attackDetailsFlyoutHeaderAssignees')).toBeInTheDocument(); + expect(screen.getByTestId('attack-details-flyout-header-assignees')).toBeInTheDocument(); expect(screen.getByTestId(HEADER_ASSIGNEES_ADD_BUTTON_TEST_ID)).toBeInTheDocument(); expect(screen.getByTestId(HEADER_ASSIGNEES_ADD_BUTTON_TEST_ID)).not.toBeDisabled(); + expect( + screen.getByTestId('attack-details-flyout-header-assignees-popover') + ).toBeInTheDocument(); }); - it('renders avatars when assignedUsers is provided', () => { + it('renders avatars when assignees are provided and user profiles loaded', () => { renderAssignees(); expect(screen.getByTestId(USERS_AVATARS_PANEL_TEST_ID)).toBeInTheDocument(); expect(screen.getByTestId(USER_AVATAR_ITEM_TEST_ID('user1'))).toBeInTheDocument(); }); - it('renders count badge when more than two assignees', () => { - mockUseAttackDetailsAssignees.mockReturnValue({ - ...defaultHookReturn, - assignedUserIds: ['uid-1', 'uid-2', 'uid-3'], - assignedUsers: [ - { uid: 'uid-1', enabled: true, user: { username: 'u1', full_name: 'U1' }, data: {} }, - { uid: 'uid-2', enabled: true, user: { username: 'u2', full_name: 'U2' }, data: {} }, - { uid: 'uid-3', enabled: true, user: { username: 'u3', full_name: 'U3' }, data: {} }, - ], - } as ReturnType); + it('does not render avatars when assignees is empty', () => { + mockUseHeaderData.mockReturnValue({ + ...defaultHeaderData, + assignees: [], + }); renderAssignees(); - expect(screen.getByTestId(USERS_AVATARS_COUNT_BADGE_TEST_ID)).toBeInTheDocument(); - expect(screen.getByTestId(USERS_AVATARS_COUNT_BADGE_TEST_ID)).toHaveTextContent('3'); - }); - - it('disables add button when loading', () => { - mockUseAttackDetailsAssignees.mockReturnValue({ - ...defaultHookReturn, - isLoading: true, - } as ReturnType); - - renderAssignees(); - - expect(screen.getByTestId(HEADER_ASSIGNEES_ADD_BUTTON_TEST_ID)).toBeDisabled(); + expect(screen.getByTestId('attack-details-flyout-header-assignees')).toBeInTheDocument(); + expect(screen.queryByTestId(USERS_AVATARS_PANEL_TEST_ID)).not.toBeInTheDocument(); + expect(screen.getByTestId(HEADER_ASSIGNEES_ADD_BUTTON_TEST_ID)).toBeInTheDocument(); }); - it('does not render avatars when assignedUsers is empty', () => { - mockUseAttackDetailsAssignees.mockReturnValue({ - ...defaultHookReturn, - assignedUserIds: [], - assignedUsers: undefined, - } as ReturnType); + it('renders empty state when privileges are loading', () => { + mockUseAttacksPrivileges.mockReturnValue({ + hasIndexWrite: true, + hasAttackIndexWrite: true, + loading: true, + }); renderAssignees(); - expect(screen.getByTestId('attackDetailsFlyoutHeaderAssignees')).toBeInTheDocument(); - expect(screen.queryByTestId(USERS_AVATARS_PANEL_TEST_ID)).not.toBeInTheDocument(); - expect(screen.getByTestId(HEADER_ASSIGNEES_ADD_BUTTON_TEST_ID)).toBeInTheDocument(); + expect(screen.getByTestId('attack-details-flyout-header-assignees-empty')).toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/components/assignees.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/components/assignees.tsx index f19afa1d52212..0a2d5239995b4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/components/assignees.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/components/assignees.tsx @@ -10,72 +10,84 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { EuiButtonIcon, + EuiContextMenu, EuiFlexGroup, EuiFlexItem, EuiPopover, + EuiPopoverTitle, EuiToolTip, - useGeneratedHtmlId, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getEmptyTagValue } from '../../../common/components/empty_value'; -import { ASSIGNEES_PANEL_WIDTH } from '../../../common/components/assignees/constants'; -import type { AssigneesApplyPanelProps } from '../../../common/components/assignees/assignees_apply_panel'; -import { AssigneesApplyPanel } from '../../../common/components/assignees/assignees_apply_panel'; import { UsersAvatarsPanel } from '../../../common/components/user_profiles/users_avatars_panel'; -import { useAttackDetailsAssignees } from '../hooks/use_attack_details_assignees'; +import { useBulkGetUserProfiles } from '../../../common/components/user_profiles/use_bulk_get_user_profiles'; +import { useLicense } from '../../../common/hooks/use_license'; +import { useUpsellingMessage } from '../../../common/hooks/use_upselling'; +import { useAttackAssigneesContextMenuItems } from '../../../detections/hooks/attacks/bulk_actions/context_menu_items/use_attack_assignees_context_menu_items'; +import { useAttacksPrivileges } from '../../../detections/hooks/attacks/bulk_actions/use_attacks_privileges'; +import { useInvalidateFindAttackDiscoveries } from '../../../attack_discovery/pages/use_find_attack_discoveries'; +import { useAttackDetailsContext } from '../context'; +import { useHeaderData } from '../hooks/use_header_data'; import { HEADER_ASSIGNEES_ADD_BUTTON_TEST_ID } from '../constants/test_ids'; -const UpdateAssigneesButton: FC<{ +const AssigneesButton: FC<{ isDisabled: boolean; toolTipMessage: string; - togglePopover: () => void; -}> = memo(({ togglePopover, isDisabled, toolTipMessage }) => ( + onClick: () => void; +}> = memo(({ onClick, isDisabled, toolTipMessage }) => ( )); -UpdateAssigneesButton.displayName = 'UpdateAssigneesButton'; +AssigneesButton.displayName = 'AssigneesButton'; /** * Assignees block for the Attack details flyout header. - * Matches the look of document_details assignees (avatars + popover with AssigneesApplyPanel). + * Follows the same pattern as status_popover_button: useAttackDetailsContext + useHeaderData + * + useAttackAssigneesContextMenuItems, with EuiPopover + EuiContextMenu. */ export const Assignees = memo(() => { - const { - assignedUserIds, - assignedUsers, - onApplyAssignees, - hasPermission, - isPlatinumPlus, - upsellingMessage, - isLoading, - } = useAttackDetailsAssignees(); + const { attackId, refetch } = useAttackDetailsContext(); + const { alertIds, assignees } = useHeaderData(); + const invalidateFindAttackDiscoveries = useInvalidateFindAttackDiscoveries(); + const { hasIndexWrite, hasAttackIndexWrite, loading: privilegesLoading } = useAttacksPrivileges(); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const upsellingMessage = useUpsellingMessage('alert_assignments'); const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const togglePopover = useCallback(() => setIsPopoverOpen((prev) => !prev), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const togglePopover = useCallback(() => { - setIsPopoverOpen((value) => !value); - }, []); - - const handleApplyAssignees = useCallback( - async (assignees) => { - setIsPopoverOpen(false); - await onApplyAssignees(assignees); - }, - [onApplyAssignees] + const attacksWithAssignees = useMemo( + () => [{ attackId, relatedAlertIds: alertIds, assignees }], + [attackId, alertIds, assignees] ); - const searchInputId = useGeneratedHtmlId({ - prefix: 'attackDetailsAssigneesSearchInput', + const onSuccess = useCallback(() => { + refetch(); + invalidateFindAttackDiscoveries(); + }, [refetch, invalidateFindAttackDiscoveries]); + + const { items, panels } = useAttackAssigneesContextMenuItems({ + attacksWithAssignees, + closePopover, + onSuccess, }); - const showAssignees = hasPermission && isPlatinumPlus; + const uids = useMemo(() => new Set(assignees), [assignees]); + const { data: assignedUsers } = useBulkGetUserProfiles({ uids }); + + const hasPermission = + Boolean(hasIndexWrite) && Boolean(hasAttackIndexWrite) && isPlatinumPlus && !privilegesLoading; const toolTipMessage = upsellingMessage ?? @@ -83,60 +95,59 @@ export const Assignees = memo(() => { defaultMessage: 'Assign attack', }); - const updateAssigneesPopover = useMemo( + const button = useMemo( () => ( - - } - isOpen={isPopoverOpen} - panelStyle={{ - minWidth: ASSIGNEES_PANEL_WIDTH, - }} - closePopover={togglePopover} - > - - + ), - [ - assignedUserIds, - handleApplyAssignees, - hasPermission, - isPlatinumPlus, - isLoading, - isPopoverOpen, - searchInputId, - togglePopover, - toolTipMessage, - ] + [togglePopover, toolTipMessage] ); - if (!showAssignees) { - return
{getEmptyTagValue()}
; + if (!hasPermission) { + return ( +
{getEmptyTagValue()}
+ ); } return ( {assignedUsers && assignedUsers.length > 0 && ( )} - {updateAssigneesPopover} + + + + {i18n.translate( + 'xpack.securitySolution.attackDetailsFlyout.header.assignees.popoverTitle', + { + defaultMessage: 'Manage assignees', + } + )} + + + + ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/hooks/use_attack_details_assignees.test.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/hooks/use_attack_details_assignees.test.ts deleted file mode 100644 index a2d2630041305..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/hooks/use_attack_details_assignees.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 { act, renderHook } from '@testing-library/react'; -import { useAttackDetailsAssignees } from './use_attack_details_assignees'; -import { useAttackDetailsContext } from '../context'; -import { useHeaderData } from './use_header_data'; -import { useInvalidateFindAttackDiscoveries } from '../../../attack_discovery/pages/use_find_attack_discoveries'; -import { useApplyAttackAssignees } from '../../../detections/hooks/attacks/bulk_actions/apply_actions/use_apply_attack_assignees'; -import { useAttacksPrivileges } from '../../../detections/hooks/attacks/bulk_actions/use_attacks_privileges'; -import { useLicense } from '../../../common/hooks/use_license'; -import { useUpsellingMessage } from '../../../common/hooks/use_upselling'; - -jest.mock('../context'); -jest.mock('./use_header_data'); -jest.mock('../../../attack_discovery/pages/use_find_attack_discoveries'); -jest.mock( - '../../../detections/hooks/attacks/bulk_actions/apply_actions/use_apply_attack_assignees' -); -jest.mock('../../../detections/hooks/attacks/bulk_actions/use_attacks_privileges'); -jest.mock('../../../common/hooks/use_license'); -jest.mock('../../../common/hooks/use_upselling'); -jest.mock('../../../common/components/user_profiles/use_bulk_get_user_profiles', () => ({ - useBulkGetUserProfiles: () => ({ data: undefined }), -})); - -const mockUseAttackDetailsContext = useAttackDetailsContext as jest.MockedFunction< - typeof useAttackDetailsContext ->; -const mockUseHeaderData = useHeaderData as jest.MockedFunction; -const mockUseInvalidateFindAttackDiscoveries = - useInvalidateFindAttackDiscoveries as jest.MockedFunction< - typeof useInvalidateFindAttackDiscoveries - >; -const mockUseApplyAttackAssignees = useApplyAttackAssignees as jest.MockedFunction< - typeof useApplyAttackAssignees ->; -const mockUseAttacksPrivileges = useAttacksPrivileges as jest.MockedFunction< - typeof useAttacksPrivileges ->; -const mockUseLicense = useLicense as jest.MockedFunction; -const mockUseUpsellingMessage = useUpsellingMessage as jest.MockedFunction< - typeof useUpsellingMessage ->; - -describe('useAttackDetailsAssignees', () => { - const mockRefetch = jest.fn(); - const mockInvalidateFindAttackDiscoveries = jest.fn(); - const mockApplyAssignees = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - mockUseAttackDetailsContext.mockReturnValue({ - attackId: 'attack-123', - refetch: mockRefetch, - } as unknown as ReturnType); - mockUseHeaderData.mockReturnValue({ - alertIds: ['alert-1', 'alert-2'], - assignees: ['uid-1'], - } as ReturnType); - mockUseInvalidateFindAttackDiscoveries.mockReturnValue(mockInvalidateFindAttackDiscoveries); - mockUseApplyAttackAssignees.mockReturnValue({ - applyAssignees: mockApplyAssignees, - }); - mockUseAttacksPrivileges.mockReturnValue({ - hasIndexWrite: true, - hasAttackIndexWrite: true, - loading: false, - }); - mockUseLicense.mockReturnValue({ - isPlatinumPlus: () => true, - } as ReturnType); - mockUseUpsellingMessage.mockReturnValue(undefined); - }); - - it('returns assignedUserIds and hasPermission from context and header data', () => { - const { result } = renderHook(() => useAttackDetailsAssignees()); - - expect(result.current.assignedUserIds).toEqual(['uid-1']); - expect(result.current.hasPermission).toBe(true); - expect(result.current.isPlatinumPlus).toBe(true); - }); - - it('calls applyAssignees with attackId, alertIds, and onSuccess that triggers refetch and invalidate', async () => { - let capturedOnSuccess: (() => void) | undefined; - mockApplyAssignees.mockImplementation(async ({ onSuccess }) => { - capturedOnSuccess = onSuccess; - }); - - const { result } = renderHook(() => useAttackDetailsAssignees()); - - await act(async () => { - await result.current.onApplyAssignees({ add: ['uid-2'], remove: [] }); - }); - - expect(mockApplyAssignees).toHaveBeenCalledWith( - expect.objectContaining({ - assignees: { add: ['uid-2'], remove: [] }, - attackIds: ['attack-123'], - relatedAlertIds: ['alert-1', 'alert-2'], - }) - ); - expect(capturedOnSuccess).toBeDefined(); - - await act(async () => { - capturedOnSuccess?.(); - }); - - expect(mockRefetch).toHaveBeenCalled(); - expect(mockInvalidateFindAttackDiscoveries).toHaveBeenCalled(); - }); - - it('returns hasPermission false when privileges are not granted', () => { - mockUseAttacksPrivileges.mockReturnValue({ - hasIndexWrite: false, - hasAttackIndexWrite: true, - loading: false, - }); - - const { result } = renderHook(() => useAttackDetailsAssignees()); - - expect(result.current.hasPermission).toBe(false); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/hooks/use_attack_details_assignees.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/hooks/use_attack_details_assignees.ts deleted file mode 100644 index a14bc6c76fc7e..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/attack_details/hooks/use_attack_details_assignees.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 { useCallback, useMemo, useState } from 'react'; - -import type { AssigneesApplyPanelProps } from '../../../common/components/assignees/assignees_apply_panel'; -import { useBulkGetUserProfiles } from '../../../common/components/user_profiles/use_bulk_get_user_profiles'; -import { useLicense } from '../../../common/hooks/use_license'; -import { useUpsellingMessage } from '../../../common/hooks/use_upselling'; -import type { AlertAssignees } from '../../../../common/api/detection_engine'; -import { useApplyAttackAssignees } from '../../../detections/hooks/attacks/bulk_actions/apply_actions/use_apply_attack_assignees'; -import { useAttacksPrivileges } from '../../../detections/hooks/attacks/bulk_actions/use_attacks_privileges'; -import { useInvalidateFindAttackDiscoveries } from '../../../attack_discovery/pages/use_find_attack_discoveries'; -import { useAttackDetailsContext } from '../context'; -import { useHeaderData } from './use_header_data'; - -export interface UseAttackDetailsAssigneesReturn { - assignedUserIds: string[]; - assignedUsers: ReturnType['data']; - onApplyAssignees: AssigneesApplyPanelProps['onApply']; - hasPermission: boolean; - isPlatinumPlus: boolean; - upsellingMessage: string | undefined; - isLoading: boolean; -} - -/** - * Hook that encapsulates assignees state, apply (useApplyAttackAssignees), refetch, - * cache invalidation, permissions, and user profiles for the attack details flyout header. - */ -export const useAttackDetailsAssignees = (): UseAttackDetailsAssigneesReturn => { - const { attackId, refetch } = useAttackDetailsContext(); - const { alertIds, assignees: assignedUserIds } = useHeaderData(); - const invalidateFindAttackDiscoveries = useInvalidateFindAttackDiscoveries(); - const { applyAssignees } = useApplyAttackAssignees(); - const { hasIndexWrite, hasAttackIndexWrite, loading: privilegesLoading } = useAttacksPrivileges(); - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const upsellingMessage = useUpsellingMessage('alert_assignments'); - - const [isLoading, setIsLoading] = useState(false); - - const uids = useMemo(() => new Set(assignedUserIds), [assignedUserIds]); - const { data: assignedUsers } = useBulkGetUserProfiles({ uids }); - - const onAssigneesUpdated = useCallback(() => { - refetch(); - invalidateFindAttackDiscoveries(); - }, [refetch, invalidateFindAttackDiscoveries]); - - const onApplyAssignees = useCallback( - async (assignees: AlertAssignees) => { - await applyAssignees({ - assignees, - attackIds: [attackId], - relatedAlertIds: alertIds, - setIsLoading, - onSuccess: onAssigneesUpdated, - }); - }, - [alertIds, attackId, applyAssignees, onAssigneesUpdated] - ); - - const hasPermission = hasIndexWrite && hasAttackIndexWrite && !privilegesLoading; - - return useMemo( - () => ({ - assignedUserIds, - assignedUsers, - onApplyAssignees, - hasPermission, - isPlatinumPlus, - upsellingMessage, - isLoading, - }), - [ - assignedUserIds, - assignedUsers, - onApplyAssignees, - hasPermission, - isPlatinumPlus, - upsellingMessage, - isLoading, - ] - ); -};