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(