diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx index fa474c4d601ad..cf1a4ebec9bb6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiPopover, EuiSelectableOption } from '@elastic/eui'; +import { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useDispatch } from 'react-redux'; @@ -62,13 +62,15 @@ export const InsertTimelinePopoverComponent: React.FC = ({ const insertTimelineButton = useMemo( () => ( - + {i18n.INSERT_TIMELINE}

}> + +
), [handleOpenPopover, isDisabled] ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts index de3e3c8e792fe..101837168350f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts @@ -25,5 +25,5 @@ export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate( ); export const INSERT_TIMELINE = i18n.translate('xpack.siem.insert.timeline.insertTimelineButton', { - defaultMessage: 'Insert Timeline…', + defaultMessage: 'Insert timeline link', }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts index 601db373f041e..a453be32480e2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts @@ -10,6 +10,46 @@ export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle defaultMessage: 'Error fetching data', }); +export const ERROR_DELETING = i18n.translate('xpack.siem.containers.case.errorDeletingTitle', { + defaultMessage: 'Error deleting data', +}); + +export const UPDATED_CASE = (caseTitle: string) => + i18n.translate('xpack.siem.containers.case.updatedCase', { + values: { caseTitle }, + defaultMessage: 'Updated "{caseTitle}"', + }); + +export const DELETED_CASES = (totalCases: number, caseTitle?: string) => + i18n.translate('xpack.siem.containers.case.deletedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Deleted {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const CLOSED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.siem.containers.case.closedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const REOPENED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.siem.containers.case.reopenedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + export const TAG_FETCH_FAILURE = i18n.translate( 'xpack.siem.containers.case.tagFetchFailDescription', { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index bb215d6ac271c..cb3df78257dc1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -114,3 +114,8 @@ export interface ActionLicense { enabledInConfig: boolean; enabledInLicense: boolean; } + +export interface DeleteCase { + id: string; + title?: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx index f1129bae9f537..7d040c49f1971 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx @@ -5,7 +5,7 @@ */ import { useCallback, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { patchCasesStatus } from './api'; import { BulkUpdateStatus, Case } from './types'; @@ -71,9 +71,22 @@ export const useUpdateCases = (): UseUpdateCase => { const patchData = async () => { try { dispatch({ type: 'FETCH_INIT' }); - await patchCasesStatus(cases, abortCtrl.signal); + const patchResponse = await patchCasesStatus(cases, abortCtrl.signal); if (!cancel) { + const resultCount = Object.keys(patchResponse).length; + const firstTitle = patchResponse[0].title; + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + const messageArgs = { + totalCases: resultCount, + caseTitle: resultCount === 1 ? firstTitle : '', + }; + const message = + resultCount && patchResponse[0].status === 'open' + ? i18n.REOPENED_CASES(messageArgs) + : i18n.CLOSED_CASES(messageArgs); + + displaySuccessToast(message, dispatchToaster); } } catch (error) { if (!cancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx index b44e01d06acaf..07e3786758aeb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -5,9 +5,10 @@ */ import { useCallback, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { deleteCases } from './api'; +import { DeleteCase } from './types'; interface DeleteState { isDisplayConfirmDeleteModal: boolean; @@ -57,9 +58,10 @@ const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { return state; } }; + interface UseDeleteCase extends DeleteState { dispatchResetIsDeleted: () => void; - handleOnDeleteConfirm: (caseIds: string[]) => void; + handleOnDeleteConfirm: (caseIds: DeleteCase[]) => void; handleToggleModal: () => void; } @@ -72,21 +74,26 @@ export const useDeleteCases = (): UseDeleteCase => { }); const [, dispatchToaster] = useStateToaster(); - const dispatchDeleteCases = useCallback((caseIds: string[]) => { + const dispatchDeleteCases = useCallback((cases: DeleteCase[]) => { let cancel = false; const abortCtrl = new AbortController(); const deleteData = async () => { try { dispatch({ type: 'FETCH_INIT' }); + const caseIds = cases.map(theCase => theCase.id); await deleteCases(caseIds, abortCtrl.signal); if (!cancel) { dispatch({ type: 'FETCH_SUCCESS', payload: true }); + displaySuccessToast( + i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''), + dispatchToaster + ); } } catch (error) { if (!cancel) { errorToToaster({ - title: i18n.ERROR_TITLE, + title: i18n.ERROR_DELETING, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 85ad4fd3fc47a..4973deef4d91a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -5,8 +5,8 @@ */ import { useReducer, useCallback } from 'react'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import { CasePatchRequest } from '../../../../../../plugins/case/common/api'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchCase } from './api'; import * as i18n from './translations'; @@ -94,6 +94,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => updateCase(response[0]); } dispatch({ type: 'FETCH_SUCCESS' }); + displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster); } } catch (error) { if (!cancel) { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index bdcb87b483851..5f61ccf68fc86 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -202,7 +202,7 @@ describe('AllCases', () => { .last() .simulate('click'); expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( - useGetCasesMockState.data.cases.map(theCase => theCase.id) + useGetCasesMockState.data.cases.map(({ id }) => ({ id })) ); }); it('Bulk close status update', () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 27316ab8427cb..dcfa1712c6ef9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -21,7 +21,7 @@ import styled, { css } from 'styled-components'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; -import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; +import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cases'; import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; @@ -107,11 +107,24 @@ export const AllCases = React.memo(() => { isDisplayConfirmDeleteModal, } = useDeleteCases(); - const { dispatchResetIsUpdated, isUpdated, updateBulkStatus } = useUpdateCases(); + // Update case + const { + dispatchResetIsUpdated, + isLoading: isUpdating, + isUpdated, + updateBulkStatus, + } = useUpdateCases(); + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); const refreshCases = useCallback(() => { refetchCases(filterOptions, queryParams); fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); }, [filterOptions, queryParams]); useEffect(() => { @@ -124,11 +137,6 @@ export const AllCases = React.memo(() => { dispatchResetIsUpdated(); } }, [isDeleted, isUpdated]); - const [deleteThisCase, setDeleteThisCase] = useState({ - title: '', - id: '', - }); - const [deleteBulk, setDeleteBulk] = useState([]); const confirmDeleteModal = useMemo( () => ( { onCancel={handleToggleModal} onConfirm={handleOnDeleteConfirm.bind( null, - deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id] + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] )} /> ), @@ -150,10 +158,20 @@ export const AllCases = React.memo(() => { setDeleteThisCase(deleteCase); }, []); - const toggleBulkDeleteModal = useCallback((deleteCases: string[]) => { - handleToggleModal(); - setDeleteBulk(deleteCases); - }, []); + const toggleBulkDeleteModal = useCallback( + (caseIds: string[]) => { + handleToggleModal(); + if (caseIds.length === 1) { + const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]); + if (singleCase) { + return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + } + } + const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id })); + setDeleteBulk(convertToDeleteCases); + }, + [selectedCases] + ); const handleUpdateCaseStatus = useCallback( (status: string) => { @@ -289,7 +307,7 @@ export const AllCases = React.memo(() => { - {(isCasesLoading || isDeleting) && !isDataEmpty && ( + {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( )} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx index 1be0d6a3b5fcc..49f5f44cba271 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx @@ -60,6 +60,6 @@ describe('CaseView actions', () => { expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([data.id]); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([{ id: data.id, title: data.title }]); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx index 1d90470eab0e1..04b79967aa36e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx @@ -34,7 +34,7 @@ const CaseViewActionsComponent: React.FC = ({ caseData }) => { isModalVisible={isDisplayConfirmDeleteModal} isPlural={false} onCancel={handleToggleModal} - onConfirm={handleOnDeleteConfirm.bind(null, [caseData.id])} + onConfirm={handleOnDeleteConfirm.bind(null, [{ id: caseData.id, title: caseData.title }])} /> ), [isDisplayConfirmDeleteModal, caseData] diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts index 0ca6bcff513fc..066145f7762c9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts @@ -22,13 +22,13 @@ export const REQUIRED_UPDATE_TO_SERVICE = i18n.translate( } ); -export const COPY_LINK_COMMENT = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', { - defaultMessage: 'click to copy comment link', +export const COPY_REFERENCE_LINK = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', { + defaultMessage: 'Copy reference link', }); export const MOVE_TO_ORIGINAL_COMMENT = i18n.translate( 'xpack.siem.case.caseView.moveToCommentAria', { - defaultMessage: 'click to highlight the reference comment', + defaultMessage: 'Highlight the referenced comment', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx index cc36e791e35b4..340e24e8fa55b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx @@ -154,6 +154,7 @@ export const UserActionItem = ({ labelQuoteAction={labelQuoteAction} labelTitle={labelTitle ?? <>} linkId={linkId} + fullName={fullName} username={username} updatedAt={updatedAt} onEdit={onEdit} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx index 94185cb4d130c..af1a1fdff26ce 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonIcon } from '@elastic/eui'; +import { + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButtonIcon, + EuiToolTip, +} from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import copy from 'copy-to-clipboard'; import { isEmpty } from 'lodash/fp'; @@ -33,6 +40,7 @@ interface UserActionTitleProps { labelQuoteAction?: string; labelTitle: JSX.Element; linkId?: string | null; + fullName?: string | null; updatedAt?: string | null; username: string; onEdit?: (id: string) => void; @@ -48,6 +56,7 @@ export const UserActionTitle = ({ labelQuoteAction, labelTitle, linkId, + fullName, username, updatedAt, onEdit, @@ -105,7 +114,9 @@ export const UserActionTitle = ({ - {username} + {fullName ?? username}

}> + {username} +
{labelTitle} @@ -137,20 +148,24 @@ export const UserActionTitle = ({ {!isEmpty(linkId) && ( - + {i18n.MOVE_TO_ORIGINAL_COMMENT}

}> + +
)} - + {i18n.COPY_REFERENCE_LINK}

}> + +
{propertyActions.length > 0 && ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index 3109f2382c362..87a446c45d891 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, + EuiToolTip, } from '@elastic/eui'; import styled, { css } from 'styled-components'; import { ElasticUser } from '../../../../containers/case/types'; @@ -40,8 +41,8 @@ const MyFlexGroup = styled(EuiFlexGroup)` const renderUsers = ( users: ElasticUser[], handleSendEmail: (emailAddress: string | undefined | null) => void -) => { - return users.map(({ fullName, username, email }, key) => ( +) => + users.map(({ fullName, username, email }, key) => ( @@ -49,11 +50,13 @@ const renderUsers = ( -

- - {username} - -

+ {fullName ?? username}

}> +

+ + {username} + +

+
@@ -63,11 +66,11 @@ const renderUsers = ( onClick={handleSendEmail.bind(null, email)} iconType="email" aria-label="email" + isDisabled={email == null} />
)); -}; export const UserList = React.memo(({ email, headline, loading, users }: UserListProps) => { const handleSendEmail = useCallback(