Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM] [Case] Design fixing #61681

Merged
merged 11 commits into from
Mar 30, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -62,13 +62,15 @@ export const InsertTimelinePopoverComponent: React.FC<Props> = ({

const insertTimelineButton = useMemo(
() => (
<EuiButtonIcon
aria-label={i18n.INSERT_TIMELINE}
data-test-subj="insert-timeline-button"
iconType="timeline"
isDisabled={isDisabled}
onClick={handleOpenPopover}
/>
<EuiToolTip position="top" content={<p>{i18n.INSERT_TIMELINE}</p>}>
<EuiButtonIcon
aria-label={i18n.INSERT_TIMELINE}
data-test-subj="insert-timeline-button"
iconType="timeline"
isDisabled={isDisabled}
onClick={handleOpenPopover}
/>
</EuiToolTip>
),
[handleOpenPopover, isDisabled]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
28 changes: 28 additions & 0 deletions x-pack/legacy/plugins/siem/public/containers/case/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,34 @@ 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: 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: 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',
{
Expand Down
5 changes: 5 additions & 0 deletions x-pack/legacy/plugins/siem/public/containers/case/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,8 @@ export interface ActionLicense {
enabledInConfig: boolean;
enabledInLicense: boolean;
}

export interface DeleteCase {
id: string;
title?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,9 +71,23 @@ 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 });
if (resultCount && patchResponse[0].status === 'open') {
displaySuccessToast(
i18n.REOPENED_CASES(resultCount, resultCount === 1 ? firstTitle : ''),
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
dispatchToaster
);
} else {
displaySuccessToast(
i18n.CLOSED_CASES(resultCount, resultCount === 1 ? firstTitle : ''),
dispatchToaster
);
}
}
} catch (error) {
if (!cancel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
handleToggleModal: () => void;
}

Expand All @@ -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,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<DeleteCase[]>([]);

const refreshCases = useCallback(() => {
refetchCases(filterOptions, queryParams);
fetchCasesStatus();
setSelectedCases([]);
setDeleteBulk([]);
}, [filterOptions, queryParams]);

useEffect(() => {
Expand All @@ -124,11 +137,6 @@ export const AllCases = React.memo(() => {
dispatchResetIsUpdated();
}
}, [isDeleted, isUpdated]);
const [deleteThisCase, setDeleteThisCase] = useState({
title: '',
id: '',
});
const [deleteBulk, setDeleteBulk] = useState<string[]>([]);
const confirmDeleteModal = useMemo(
() => (
<ConfirmDeleteCaseModal
Expand All @@ -138,7 +146,7 @@ export const AllCases = React.memo(() => {
onCancel={handleToggleModal}
onConfirm={handleOnDeleteConfirm.bind(
null,
deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id]
deleteBulk.length > 0 ? deleteBulk : [deleteThisCase]
)}
/>
),
Expand All @@ -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) => {
Expand Down Expand Up @@ -289,7 +307,7 @@ export const AllCases = React.memo(() => {
</EuiFlexItem>
</EuiFlexGroup>
</CaseHeaderPage>
{(isCasesLoading || isDeleting) && !isDataEmpty && (
{(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && (
<ProgressLoader size="xs" color="accent" className="essentialAnimation" />
)}
<Panel loading={isCasesLoading}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const CaseViewActionsComponent: React.FC<CaseViewActions> = ({ 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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export const UserActionItem = ({
labelQuoteAction={labelQuoteAction}
labelTitle={labelTitle ?? <></>}
linkId={linkId}
fullName={fullName}
username={username}
updatedAt={updatedAt}
onEdit={onEdit}
Expand Down
Loading