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 65d94865bf00ca..5b6ff8438be8c9 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
@@ -18,6 +18,8 @@ export interface Comment {
export interface Case {
id: string;
+ closedAt: string | null;
+ closedBy: ElasticUser | null;
comments: Comment[];
commentIds: string[];
createdAt: string;
@@ -59,12 +61,13 @@ export interface AllCases extends CasesStatus {
export enum SortFieldCase {
createdAt = 'createdAt',
- updatedAt = 'updatedAt',
+ closedAt = 'closedAt',
}
export interface ElasticUser {
- readonly username: string;
+ readonly email?: string | null;
readonly fullName?: string | null;
+ readonly username: string;
}
export interface FetchCasesProps {
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
index a179b6f546b9bc..b70195e2c126f5 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
@@ -49,6 +49,8 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
};
const initialData: Case = {
id: '',
+ closedAt: null,
+ closedBy: null,
createdAt: '',
comments: [],
commentIds: [],
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 afcbe20fa791ad..987620469901bd 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,7 +5,7 @@
*/
import { useReducer, useCallback } from 'react';
-
+import { cloneDeep } from 'lodash/fp';
import { CaseRequest } from '../../../../../../plugins/case/common/api';
import { errorToToaster, useStateToaster } from '../../components/toasters';
@@ -47,7 +47,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState =>
...state,
isLoading: false,
isError: false,
- caseData: action.payload,
+ caseData: cloneDeep(action.payload),
updateKey: null,
};
case 'FETCH_FAILURE':
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
index 0fe8daafcb30ab..5d00b770b3ca9c 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
@@ -13,6 +13,8 @@ export const useGetCasesMockState: UseGetCasesState = {
countOpenCases: 0,
cases: [
{
+ closedAt: null,
+ closedBy: null,
id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:23.627Z',
createdBy: { username: 'elastic' },
@@ -27,6 +29,8 @@ export const useGetCasesMockState: UseGetCasesState = {
version: 'WzQ3LDFd',
},
{
+ closedAt: null,
+ closedBy: null,
id: '362a5c10-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:13.328Z',
createdBy: { username: 'elastic' },
@@ -41,6 +45,8 @@ export const useGetCasesMockState: UseGetCasesState = {
version: 'WzQ3LDFd',
},
{
+ closedAt: null,
+ closedBy: null,
id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:11.328Z',
createdBy: { username: 'elastic' },
@@ -55,6 +61,8 @@ export const useGetCasesMockState: UseGetCasesState = {
version: 'WzQ3LDFd',
},
{
+ closedAt: '2020-02-13T19:44:13.328Z',
+ closedBy: { username: 'elastic' },
id: '31890e90-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:05.563Z',
createdBy: { username: 'elastic' },
@@ -64,11 +72,13 @@ export const useGetCasesMockState: UseGetCasesState = {
status: 'closed',
tags: ['phishing'],
title: 'Uh oh',
- updatedAt: null,
- updatedBy: null,
+ updatedAt: '2020-02-13T19:44:13.328Z',
+ updatedBy: { username: 'elastic' },
version: 'WzQ3LDFd',
},
{
+ closedAt: null,
+ closedBy: null,
id: '2f5b3210-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:01.901Z',
createdBy: { username: 'elastic' },
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx
index 5859e6bbce263f..b9e1113c486ad9 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx
@@ -36,7 +36,8 @@ const Spacer = styled.span`
const renderStringField = (field: string, dataTestSubj: string) =>
field != null ? {field} : getEmptyTagValue();
export const getCasesColumns = (
- actions: Array>
+ actions: Array>,
+ filterStatus: string
): CasesColumns[] => [
{
name: i18n.NAME,
@@ -113,22 +114,39 @@ export const getCasesColumns = (
render: (comments: Case['commentIds']) =>
renderStringField(`${comments.length}`, `case-table-column-commentCount`),
},
- {
- field: 'createdAt',
- name: i18n.OPENED_ON,
- sortable: true,
- render: (createdAt: Case['createdAt']) => {
- if (createdAt != null) {
- return (
-
- );
+ filterStatus === 'open'
+ ? {
+ field: 'createdAt',
+ name: i18n.OPENED_ON,
+ sortable: true,
+ render: (createdAt: Case['createdAt']) => {
+ if (createdAt != null) {
+ return (
+
+ );
+ }
+ return getEmptyTagValue();
+ },
}
- return getEmptyTagValue();
- },
- },
+ : {
+ field: 'closedAt',
+ name: i18n.CLOSED_ON,
+ sortable: true,
+ render: (closedAt: Case['closedAt']) => {
+ if (closedAt != null) {
+ return (
+
+ );
+ }
+ return getEmptyTagValue();
+ },
+ },
{
name: 'Actions',
actions,
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 9f836bd043c9dc..9a84dd07b0af44 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
@@ -71,8 +71,8 @@ const ProgressLoader = styled(EuiProgress)`
const getSortField = (field: string): SortFieldCase => {
if (field === SortFieldCase.createdAt) {
return SortFieldCase.createdAt;
- } else if (field === SortFieldCase.updatedAt) {
- return SortFieldCase.updatedAt;
+ } else if (field === SortFieldCase.closedAt) {
+ return SortFieldCase.closedAt;
}
return SortFieldCase.createdAt;
};
@@ -206,17 +206,25 @@ export const AllCases = React.memo(() => {
}
setQueryParams(newQueryParams);
},
- [setQueryParams, queryParams]
+ [queryParams]
);
const onFilterChangedCallback = useCallback(
(newFilterOptions: Partial) => {
+ if (newFilterOptions.status && newFilterOptions.status === 'closed') {
+ setQueryParams({ ...queryParams, sortField: SortFieldCase.closedAt });
+ } else if (newFilterOptions.status && newFilterOptions.status === 'open') {
+ setQueryParams({ ...queryParams, sortField: SortFieldCase.createdAt });
+ }
setFilters({ ...filterOptions, ...newFilterOptions });
},
- [filterOptions, setFilters]
+ [filterOptions, queryParams]
);
- const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [actions]);
+ const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions, filterOptions.status), [
+ actions,
+ filterOptions.status,
+ ]);
const memoizedPagination = useMemo(
() => ({
pageIndex: queryParams.page - 1,
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts
index 27532e57166e1d..8f79b78ef7568b 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts
@@ -60,9 +60,3 @@ export const CLOSED = i18n.translate('xpack.siem.case.caseTable.closed', {
export const DELETE = i18n.translate('xpack.siem.case.caseTable.delete', {
defaultMessage: 'Delete',
});
-export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase', {
- defaultMessage: 'Reopen case',
-});
-export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', {
- defaultMessage: 'Close case',
-});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx
new file mode 100644
index 00000000000000..9dbd71ea3e34c2
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback } from 'react';
+import styled, { css } from 'styled-components';
+import {
+ EuiBadge,
+ EuiButtonToggle,
+ EuiDescriptionList,
+ EuiDescriptionListDescription,
+ EuiDescriptionListTitle,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+import * as i18n from '../case_view/translations';
+import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date';
+import { CaseViewActions } from '../case_view/actions';
+
+const MyDescriptionList = styled(EuiDescriptionList)`
+ ${({ theme }) => css`
+ & {
+ padding-right: ${theme.eui.euiSizeL};
+ border-right: ${theme.eui.euiBorderThin};
+ }
+ `}
+`;
+
+interface CaseStatusProps {
+ 'data-test-subj': string;
+ badgeColor: string;
+ buttonLabel: string;
+ caseId: string;
+ caseTitle: string;
+ icon: string;
+ isLoading: boolean;
+ isSelected: boolean;
+ status: string;
+ title: string;
+ toggleStatusCase: (status: string) => void;
+ value: string | null;
+}
+const CaseStatusComp: React.FC = ({
+ 'data-test-subj': dataTestSubj,
+ badgeColor,
+ buttonLabel,
+ caseId,
+ caseTitle,
+ icon,
+ isLoading,
+ isSelected,
+ status,
+ title,
+ toggleStatusCase,
+ value,
+}) => {
+ const onChange = useCallback(e => toggleStatusCase(e.target.checked ? 'closed' : 'open'), [
+ toggleStatusCase,
+ ]);
+ return (
+
+
+
+
+
+ {i18n.STATUS}
+
+
+ {status}
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const CaseStatus = React.memo(CaseStatusComp);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
index 53cc1f80b5c108..e11441eac3a9d2 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
@@ -10,6 +10,8 @@ import { Case } from '../../../../../containers/case/types';
export const caseProps: CaseProps = {
caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
initialData: {
+ closedAt: null,
+ closedBy: null,
id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'],
comments: [
@@ -20,6 +22,7 @@ export const caseProps: CaseProps = {
createdBy: {
fullName: 'Steph Milovic',
username: 'smilovic',
+ email: 'notmyrealemailfool@elastic.co',
},
updatedAt: '2020-02-20T23:06:33.798Z',
updatedBy: {
@@ -29,7 +32,7 @@ export const caseProps: CaseProps = {
},
],
createdAt: '2020-02-13T19:44:23.627Z',
- createdBy: { fullName: null, username: 'elastic' },
+ createdBy: { fullName: null, email: 'testemail@elastic.co', username: 'elastic' },
description: 'Security banana Issue',
status: 'open',
tags: ['defacement'],
@@ -41,35 +44,22 @@ export const caseProps: CaseProps = {
version: 'WzQ3LDFd',
},
};
-
-export const data: Case = {
- id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
- commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'],
- comments: [
- {
- comment: 'Solve this fast!',
- id: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8',
- createdAt: '2020-02-20T23:06:33.798Z',
- createdBy: {
- fullName: 'Steph Milovic',
- username: 'smilovic',
- },
- updatedAt: '2020-02-20T23:06:33.798Z',
- updatedBy: {
- username: 'elastic',
- },
- version: 'WzQ3LDFd',
+export const caseClosedProps: CaseProps = {
+ ...caseProps,
+ initialData: {
+ ...caseProps.initialData,
+ closedAt: '2020-02-20T23:06:33.798Z',
+ closedBy: {
+ username: 'elastic',
},
- ],
- createdAt: '2020-02-13T19:44:23.627Z',
- createdBy: { username: 'elastic', fullName: null },
- description: 'Security banana Issue',
- status: 'open',
- tags: ['defacement'],
- title: 'Another horrible breach!!',
- updatedAt: '2020-02-19T15:02:57.995Z',
- updatedBy: {
- username: 'elastic',
+ status: 'closed',
},
- version: 'WzQ3LDFd',
+};
+
+export const data: Case = {
+ ...caseProps.initialData,
+};
+
+export const dataClosed: Case = {
+ ...caseClosedProps.initialData,
};
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
new file mode 100644
index 00000000000000..4e1e5ba753c364
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import { CaseViewActions } from './actions';
+import { TestProviders } from '../../../../mock';
+import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
+jest.mock('../../../../containers/case/use_delete_cases');
+const useDeleteCasesMock = useDeleteCases as jest.Mock;
+
+describe('CaseView actions', () => {
+ const caseTitle = 'Cool title';
+ const caseId = 'cool-id';
+ const handleOnDeleteConfirm = jest.fn();
+ const handleToggleModal = jest.fn();
+ const dispatchResetIsDeleted = jest.fn();
+ const defaultDeleteState = {
+ dispatchResetIsDeleted,
+ handleToggleModal,
+ handleOnDeleteConfirm,
+ isLoading: false,
+ isError: false,
+ isDeleted: false,
+ isDisplayConfirmDeleteModal: false,
+ };
+ beforeEach(() => {
+ jest.resetAllMocks();
+ useDeleteCasesMock.mockImplementation(() => defaultDeleteState);
+ });
+ it('clicking trash toggles modal', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy();
+
+ wrapper
+ .find('button[data-test-subj="property-actions-ellipses"]')
+ .first()
+ .simulate('click');
+ wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click');
+ expect(handleToggleModal).toHaveBeenCalled();
+ });
+ it('toggle delete modal and confirm', () => {
+ useDeleteCasesMock.mockImplementation(() => ({
+ ...defaultDeleteState,
+ isDisplayConfirmDeleteModal: true,
+ }));
+ const wrapper = mount(
+
+
+
+ );
+
+ 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([caseId]);
+ });
+});
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
new file mode 100644
index 00000000000000..88a717ac5fa6a9
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useMemo } from 'react';
+
+import { Redirect } from 'react-router-dom';
+import * as i18n from './translations';
+import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
+import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
+import { SiemPageName } from '../../../home/types';
+import { PropertyActions } from '../property_actions';
+
+interface CaseViewActions {
+ caseId: string;
+ caseTitle: string;
+}
+
+const CaseViewActionsComponent: React.FC = ({ caseId, caseTitle }) => {
+ // Delete case
+ const {
+ handleToggleModal,
+ handleOnDeleteConfirm,
+ isDeleted,
+ isDisplayConfirmDeleteModal,
+ } = useDeleteCases();
+
+ const confirmDeleteModal = useMemo(
+ () => (
+
+ ),
+ [isDisplayConfirmDeleteModal]
+ );
+ // TO DO refactor each of these const's into their own components
+ const propertyActions = useMemo(
+ () => [
+ {
+ iconType: 'trash',
+ label: i18n.DELETE_CASE,
+ onClick: handleToggleModal,
+ },
+ {
+ iconType: 'popout',
+ label: 'View ServiceNow incident',
+ onClick: () => null,
+ },
+ {
+ iconType: 'importAction',
+ label: 'Update ServiceNow incident',
+ onClick: () => null,
+ },
+ ],
+ [handleToggleModal]
+ );
+
+ if (isDeleted) {
+ return ;
+ }
+ return (
+ <>
+
+ {confirmDeleteModal}
+ >
+ );
+};
+
+export const CaseViewActions = React.memo(CaseViewActionsComponent);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
index 15d6cf7cf7317a..ec18bdb2bf9abe 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
@@ -7,15 +7,13 @@
import React from 'react';
import { mount } from 'enzyme';
import { CaseComponent } from './';
-import * as updateHook from '../../../../containers/case/use_update_case';
-import * as deleteHook from '../../../../containers/case/use_delete_cases';
-import { caseProps, data } from './__mock__';
+import { caseProps, caseClosedProps, data, dataClosed } from './__mock__';
import { TestProviders } from '../../../../mock';
+import { useUpdateCase } from '../../../../containers/case/use_update_case';
+jest.mock('../../../../containers/case/use_update_case');
+const useUpdateCaseMock = useUpdateCase as jest.Mock;
describe('CaseView ', () => {
- const handleOnDeleteConfirm = jest.fn();
- const handleToggleModal = jest.fn();
- const dispatchResetIsDeleted = jest.fn();
const updateCaseProperty = jest.fn();
/* eslint-disable no-console */
// Silence until enzyme fixed to use ReactTestUtils.act()
@@ -28,15 +26,17 @@ describe('CaseView ', () => {
});
/* eslint-enable no-console */
+ const defaultUpdateCaseState = {
+ caseData: data,
+ isLoading: false,
+ isError: false,
+ updateKey: null,
+ updateCaseProperty,
+ };
+
beforeEach(() => {
jest.resetAllMocks();
- jest.spyOn(updateHook, 'useUpdateCase').mockReturnValue({
- caseData: data,
- isLoading: false,
- isError: false,
- updateKey: null,
- updateCaseProperty,
- });
+ useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState);
});
it('should render CaseComponent', () => {
@@ -69,6 +69,7 @@ describe('CaseView ', () => {
.first()
.text()
).toEqual(data.createdBy.username);
+ expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false);
expect(
wrapper
.find(`[data-test-subj="case-view-createdAt"]`)
@@ -82,6 +83,30 @@ describe('CaseView ', () => {
.prop('raw')
).toEqual(data.description);
});
+ it('should show closed indicators in header when case is closed', () => {
+ useUpdateCaseMock.mockImplementation(() => ({
+ ...defaultUpdateCaseState,
+ caseData: dataClosed,
+ }));
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false);
+ expect(
+ wrapper
+ .find(`[data-test-subj="case-view-closedAt"]`)
+ .first()
+ .prop('value')
+ ).toEqual(dataClosed.closedAt);
+ expect(
+ wrapper
+ .find(`[data-test-subj="case-view-status"]`)
+ .first()
+ .text()
+ ).toEqual(dataClosed.status);
+ });
it('should dispatch update state when button is toggled', () => {
const wrapper = mount(
@@ -92,7 +117,7 @@ describe('CaseView ', () => {
wrapper
.find('input[data-test-subj="toggle-case-status"]')
- .simulate('change', { target: { value: false } });
+ .simulate('change', { target: { checked: true } });
expect(updateCaseProperty).toBeCalledWith({
updateKey: 'status',
@@ -133,46 +158,4 @@ describe('CaseView ', () => {
.prop('source')
).toEqual(data.comments[0].comment);
});
-
- it('toggle delete modal and cancel', () => {
- const wrapper = mount(
-
-
-
- );
-
- expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy();
-
- wrapper
- .find(
- '[data-test-subj="case-view-actions"] button[data-test-subj="property-actions-ellipses"]'
- )
- .first()
- .simulate('click');
- wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click');
- expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy();
- wrapper.find('button[data-test-subj="confirmModalCancelButton"]').simulate('click');
- expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy();
- });
-
- it('toggle delete modal and confirm', () => {
- jest.spyOn(deleteHook, 'useDeleteCases').mockReturnValue({
- dispatchResetIsDeleted,
- handleToggleModal,
- handleOnDeleteConfirm,
- isLoading: false,
- isError: false,
- isDeleted: false,
- isDisplayConfirmDeleteModal: true,
- });
- const wrapper = mount(
-
-
-
- );
-
- 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([caseProps.caseId]);
- });
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
index 82216e88a091e7..dce7bde2225c92 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
@@ -5,26 +5,14 @@
*/
import React, { useCallback, useMemo } from 'react';
-import {
- EuiBadge,
- EuiButtonToggle,
- EuiDescriptionList,
- EuiDescriptionListDescription,
- EuiDescriptionListTitle,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
-import styled, { css } from 'styled-components';
-import { Redirect } from 'react-router-dom';
+import styled from 'styled-components';
import * as i18n from './translations';
import { Case } from '../../../../containers/case/types';
-import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date';
import { getCaseUrl } from '../../../../components/link_to';
import { HeaderPage } from '../../../../components/header_page';
import { EditableTitle } from '../../../../components/header_page/editable_title';
-import { PropertyActions } from '../property_actions';
import { TagList } from '../tag_list';
import { useGetCase } from '../../../../containers/case/use_get_case';
import { UserActionTree } from '../user_action_tree';
@@ -33,23 +21,13 @@ import { useUpdateCase } from '../../../../containers/case/use_update_case';
import { WrapperPage } from '../../../../components/wrapper_page';
import { getTypedPayload } from '../../../../containers/case/utils';
import { WhitePageWrapper } from '../wrappers';
-import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
-import { SiemPageName } from '../../../home/types';
-import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
+import { useBasePath } from '../../../../lib/kibana';
+import { CaseStatus } from '../case_status';
interface Props {
caseId: string;
}
-const MyDescriptionList = styled(EuiDescriptionList)`
- ${({ theme }) => css`
- & {
- padding-right: ${theme.eui.euiSizeL};
- border-right: ${theme.eui.euiBorderThin};
- }
- `}
-`;
-
const MyWrapper = styled(WrapperPage)`
padding-bottom: 0;
`;
@@ -64,6 +42,8 @@ export interface CaseProps {
}
export const CaseComponent = React.memo(({ caseId, initialData }) => {
+ const basePath = window.location.origin + useBasePath();
+ const caseLink = `${basePath}/app/siem#/case/${caseId}`;
const { caseData, isLoading, updateKey, updateCaseProperty } = useUpdateCase(caseId, initialData);
// Update Fields
@@ -107,58 +87,44 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
return null;
}
},
- [updateCaseProperty, caseData.status]
- );
- const toggleStatusCase = useCallback(
- e => onUpdateField('status', e.target.checked ? 'open' : 'closed'),
- [onUpdateField]
+ [caseData.status]
);
- const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]);
const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]);
-
- // Delete case
- const {
- handleToggleModal,
- handleOnDeleteConfirm,
- isDeleted,
- isDisplayConfirmDeleteModal,
- } = useDeleteCases();
-
- const confirmDeleteModal = useMemo(
- () => (
-
- ),
- [isDisplayConfirmDeleteModal]
+ const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]);
+ const toggleStatusCase = useCallback(status => onUpdateField('status', status), [onUpdateField]);
+
+ const caseStatusData = useMemo(
+ () =>
+ caseData.status === 'open'
+ ? {
+ 'data-test-subj': 'case-view-createdAt',
+ value: caseData.createdAt,
+ title: i18n.CASE_OPENED,
+ buttonLabel: i18n.CLOSE_CASE,
+ status: caseData.status,
+ icon: 'checkInCircleFilled',
+ badgeColor: 'secondary',
+ isSelected: false,
+ }
+ : {
+ 'data-test-subj': 'case-view-closedAt',
+ value: caseData.closedAt,
+ title: i18n.CASE_CLOSED,
+ buttonLabel: i18n.REOPEN_CASE,
+ status: caseData.status,
+ icon: 'magnet',
+ badgeColor: 'danger',
+ isSelected: true,
+ },
+ [caseData.closedAt, caseData.createdAt, caseData.status]
+ );
+ const emailContent = useMemo(
+ () => ({
+ subject: i18n.EMAIL_SUBJECT(caseData.title),
+ body: i18n.EMAIL_BODY(caseLink),
+ }),
+ [caseData.title]
);
- // TO DO refactor each of these const's into their own components
- const propertyActions = [
- {
- iconType: 'trash',
- label: 'Delete case',
- onClick: handleToggleModal,
- },
- {
- iconType: 'popout',
- label: 'View ServiceNow incident',
- onClick: () => null,
- },
- {
- iconType: 'importAction',
- label: 'Update ServiceNow incident',
- onClick: () => null,
- },
- ];
-
- if (isDeleted) {
- return ;
- }
-
return (
<>
@@ -177,51 +143,13 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
}
title={caseData.title}
>
-
-
-
-
-
- {i18n.STATUS}
-
-
- {caseData.status}
-
-
-
-
- {i18n.CASE_OPENED}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -237,6 +165,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
@@ -250,7 +179,6 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
- {confirmDeleteModal}
>
);
});
@@ -273,4 +201,5 @@ export const CaseView = React.memo(({ caseId }: Props) => {
return ;
});
+CaseComponent.displayName = 'CaseComponent';
CaseView.displayName = 'CaseView';
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
index 82b5e771e21513..e5fa3bff51f852 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
@@ -55,3 +55,19 @@ export const STATUS = i18n.translate('xpack.siem.case.caseView.statusLabel', {
export const CASE_OPENED = i18n.translate('xpack.siem.case.caseView.caseOpened', {
defaultMessage: 'Case opened',
});
+
+export const CASE_CLOSED = i18n.translate('xpack.siem.case.caseView.caseClosed', {
+ defaultMessage: 'Case closed',
+});
+
+export const EMAIL_SUBJECT = (caseTitle: string) =>
+ i18n.translate('xpack.siem.case.caseView.emailSubject', {
+ values: { caseTitle },
+ defaultMessage: 'SIEM Case - {caseTitle}',
+ });
+
+export const EMAIL_BODY = (caseUrl: string) =>
+ i18n.translate('xpack.siem.case.caseView.emailBody', {
+ values: { caseUrl },
+ defaultMessage: 'Case reference: {caseUrl}',
+ });
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx
new file mode 100644
index 00000000000000..51acb3b810d92e
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { UserList } from './';
+import * as i18n from '../case_view/translations';
+
+describe('UserList ', () => {
+ const title = 'Case Title';
+ const caseLink = 'http://reddit.com';
+ const user = { username: 'username', fullName: 'Full Name', email: 'testemail@elastic.co' };
+ const open = jest.fn();
+ beforeAll(() => {
+ window.open = open;
+ });
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+ it('triggers mailto when email icon clicked', () => {
+ const wrapper = shallow(
+
+ );
+ wrapper.find('[data-test-subj="user-list-email-button"]').simulate('click');
+ expect(open).toBeCalledWith(
+ `mailto:${user.email}?subject=${i18n.EMAIL_SUBJECT(title)}&body=${i18n.EMAIL_BODY(caseLink)}`,
+ '_blank'
+ );
+ });
+});
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 abb49122dc1421..74a1b98c29eefa 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
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import {
EuiButtonIcon,
EuiText,
@@ -17,6 +17,10 @@ import styled, { css } from 'styled-components';
import { ElasticUser } from '../../../../containers/case/types';
interface UserListProps {
+ email: {
+ subject: string;
+ body: string;
+ };
headline: string;
users: ElasticUser[];
}
@@ -31,8 +35,11 @@ const MyFlexGroup = styled(EuiFlexGroup)`
`}
`;
-const renderUsers = (users: ElasticUser[]) => {
- return users.map(({ fullName, username }, key) => (
+const renderUsers = (
+ users: ElasticUser[],
+ handleSendEmail: (emailAddress: string | undefined | null) => void
+) => {
+ return users.map(({ fullName, username, email }, key) => (
@@ -50,7 +57,8 @@ const renderUsers = (users: ElasticUser[]) => {
{}} // TO DO
+ data-test-subj="user-list-email-button"
+ onClick={handleSendEmail.bind(null, email)} // TO DO
iconType="email"
aria-label="email"
/>
@@ -59,12 +67,20 @@ const renderUsers = (users: ElasticUser[]) => {
));
};
-export const UserList = React.memo(({ headline, users }: UserListProps) => {
+export const UserList = React.memo(({ email, headline, users }: UserListProps) => {
+ const handleSendEmail = useCallback(
+ (emailAddress: string | undefined | null) => {
+ if (emailAddress && emailAddress != null) {
+ window.open(`mailto:${emailAddress}?subject=${email.subject}&body=${email.body}`, '_blank');
+ }
+ },
+ [email.subject]
+ );
return (
{headline}
- {renderUsers(users)}
+ {renderUsers(users, handleSendEmail)}
);
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
index 6ef412d408ae5d..341a34240fe499 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
@@ -30,6 +30,16 @@ export const OPENED_ON = i18n.translate('xpack.siem.case.caseView.openedOn', {
defaultMessage: 'Opened on',
});
+export const CLOSED_ON = i18n.translate('xpack.siem.case.caseView.closedOn', {
+ defaultMessage: 'Closed on',
+});
+export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase', {
+ defaultMessage: 'Reopen case',
+});
+export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', {
+ defaultMessage: 'Close case',
+});
+
export const REPORTER = i18n.translate('xpack.siem.case.caseView.createdBy', {
defaultMessage: 'Reporter',
});
diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts
index 68a222cb656ed0..6f58e2702ec5bd 100644
--- a/x-pack/plugins/case/common/api/cases/case.ts
+++ b/x-pack/plugins/case/common/api/cases/case.ts
@@ -24,6 +24,8 @@ export const CaseAttributesRt = rt.intersection([
CaseBasicRt,
rt.type({
comment_ids: rt.array(rt.string),
+ closed_at: rt.union([rt.string, rt.null]),
+ closed_by: rt.union([UserRT, rt.null]),
created_at: rt.string,
created_by: UserRT,
updated_at: rt.union([rt.string, rt.null]),
diff --git a/x-pack/plugins/case/common/api/user.ts b/x-pack/plugins/case/common/api/user.ts
index ed44791c4e04d2..651cd08f08a021 100644
--- a/x-pack/plugins/case/common/api/user.ts
+++ b/x-pack/plugins/case/common/api/user.ts
@@ -7,6 +7,7 @@
import * as rt from 'io-ts';
export const UserRT = rt.type({
+ email: rt.union([rt.undefined, rt.string]),
full_name: rt.union([rt.undefined, rt.string]),
username: rt.string,
});
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts
index 17a25184826378..c08dae1dc18b49 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts
@@ -13,7 +13,11 @@ function createAuthenticationMock({
authc.getCurrentUser.mockReturnValue(
currentUser !== undefined
? currentUser
- : ({ username: 'awesome', full_name: 'Awesome D00d' } as AuthenticatedUser)
+ : ({
+ email: 'd00d@awesome.com',
+ username: 'awesome',
+ full_name: 'Awesome D00d',
+ } as AuthenticatedUser)
);
return authc;
}
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
index 1e1965f83ff684..5aa8b93f17b080 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
@@ -12,10 +12,13 @@ export const mockCases: Array> = [
type: 'cases',
id: 'mock-id-1',
attributes: {
+ closed_at: null,
+ closed_by: null,
comment_ids: ['mock-comment-1'],
created_at: '2019-11-25T21:54:48.952Z',
created_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
description: 'This is a brand new case of a bad meanie defacing data',
@@ -25,6 +28,7 @@ export const mockCases: Array> = [
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
},
@@ -36,10 +40,13 @@ export const mockCases: Array> = [
type: 'cases',
id: 'mock-id-2',
attributes: {
+ closed_at: null,
+ closed_by: null,
comment_ids: [],
created_at: '2019-11-25T22:32:00.900Z',
created_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
description: 'Oh no, a bad meanie destroying data!',
@@ -49,6 +56,7 @@ export const mockCases: Array> = [
updated_at: '2019-11-25T22:32:00.900Z',
updated_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
},
@@ -60,10 +68,13 @@ export const mockCases: Array> = [
type: 'cases',
id: 'mock-id-3',
attributes: {
+ closed_at: null,
+ closed_by: null,
comment_ids: [],
created_at: '2019-11-25T22:32:17.947Z',
created_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
description: 'Oh no, a bad meanie going LOLBins all over the place!',
@@ -73,6 +84,39 @@ export const mockCases: Array> = [
updated_at: '2019-11-25T22:32:17.947Z',
updated_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ },
+ references: [],
+ updated_at: '2019-11-25T22:32:17.947Z',
+ version: 'WzUsMV0=',
+ },
+ {
+ type: 'cases',
+ id: 'mock-id-4',
+ attributes: {
+ closed_at: '2019-11-25T22:32:17.947Z',
+ closed_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ comment_ids: [],
+ created_at: '2019-11-25T22:32:17.947Z',
+ created_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ description: 'Oh no, a bad meanie going LOLBins all over the place!',
+ title: 'Another bad one',
+ status: 'closed',
+ tags: ['LOLBins'],
+ updated_at: '2019-11-25T22:32:17.947Z',
+ updated_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
},
@@ -100,11 +144,13 @@ export const mockCaseComments: Array> = [
created_at: '2019-11-25T21:55:00.177Z',
created_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
updated_at: '2019-11-25T21:55:00.177Z',
updated_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
},
@@ -126,11 +172,13 @@ export const mockCaseComments: Array> = [
created_at: '2019-11-25T21:55:14.633Z',
created_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
updated_at: '2019-11-25T21:55:14.633Z',
updated_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
},
@@ -153,11 +201,13 @@ export const mockCaseComments: Array> = [
created_at: '2019-11-25T22:32:30.608Z',
created_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
updated_at: '2019-11-25T22:32:30.608Z',
updated_by: {
full_name: 'elastic',
+ email: 'testemail@elastic.co',
username: 'elastic',
},
},
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
index 0166ba89eb76c8..c14a94e84e51c3 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
@@ -56,14 +56,14 @@ export function initPatchCommentApi({ caseService, router }: RouteDeps) {
}
const updatedBy = await caseService.getUser({ request, response });
- const { full_name, username } = updatedBy;
+ const { email, full_name, username } = updatedBy;
const updatedComment = await caseService.patchComment({
client: context.core.savedObjects.client,
commentId: query.id,
updatedAttributes: {
comment: query.comment,
updated_at: new Date().toISOString(),
- updated_by: { full_name, username },
+ updated_by: { email, full_name, username },
},
version: query.version,
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
index 1da1161ab01d19..1542394fc438db 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
@@ -49,7 +49,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
}
const updatedBy = await caseService.getUser({ request, response });
- const { full_name, username } = updatedBy;
+ const { email, full_name, username } = updatedBy;
const updateDate = new Date().toISOString();
const patch = await caseConfigureService.patch({
@@ -58,7 +58,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
updatedAttributes: {
...queryWithoutVersion,
updated_at: updateDate,
- updated_by: { full_name, username },
+ updated_by: { email, full_name, username },
},
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
index a22dd8437e5087..c839d36dcf4dfb 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
@@ -43,7 +43,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
);
}
const updatedBy = await caseService.getUser({ request, response });
- const { full_name, username } = updatedBy;
+ const { email, full_name, username } = updatedBy;
const creationDate = new Date().toISOString();
const post = await caseConfigureService.post({
@@ -51,7 +51,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
attributes: {
...query,
created_at: creationDate,
- created_by: { full_name, username },
+ created_by: { email, full_name, username },
updated_at: null,
updated_by: null,
},
diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts
index 7ce37d2569e573..8fafb1af0eb826 100644
--- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts
@@ -34,6 +34,6 @@ describe('GET all cases', () => {
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
- expect(response.payload.cases).toHaveLength(3);
+ expect(response.payload.cases).toHaveLength(4);
});
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
index 7ab7212d2f436e..19ff7f0734a777 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
@@ -25,7 +25,7 @@ describe('PATCH cases', () => {
toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'),
}));
});
- it(`Patch a case`, async () => {
+ it(`Close a case`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases',
method: 'patch',
@@ -50,17 +50,61 @@ describe('PATCH cases', () => {
expect(response.status).toEqual(200);
expect(response.payload).toEqual([
{
+ closed_at: '2019-11-25T21:54:48.952Z',
+ closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
comment_ids: ['mock-comment-1'],
comments: [],
created_at: '2019-11-25T21:54:48.952Z',
- created_by: { full_name: 'elastic', username: 'elastic' },
+ created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
description: 'This is a brand new case of a bad meanie defacing data',
id: 'mock-id-1',
status: 'closed',
tags: ['defacement'],
title: 'Super Bad Security Issue',
updated_at: '2019-11-25T21:54:48.952Z',
- updated_by: { full_name: 'Awesome D00d', username: 'awesome' },
+ updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
+ version: 'WzE3LDFd',
+ },
+ ]);
+ });
+ it(`Open a case`, async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path: '/api/cases',
+ method: 'patch',
+ body: {
+ cases: [
+ {
+ id: 'mock-id-4',
+ status: 'open',
+ version: 'WzUsMV0=',
+ },
+ ],
+ },
+ });
+
+ const theContext = createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ })
+ );
+
+ const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ expect(response.status).toEqual(200);
+ expect(response.payload).toEqual([
+ {
+ closed_at: null,
+ closed_by: null,
+ comment_ids: [],
+ comments: [],
+ created_at: '2019-11-25T22:32:17.947Z',
+ created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
+ description: 'Oh no, a bad meanie going LOLBins all over the place!',
+ id: 'mock-id-4',
+ status: 'open',
+ tags: ['LOLBins'],
+ title: 'Another bad one',
+ updated_at: '2019-11-25T21:54:48.952Z',
+ updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
version: 'WzE3LDFd',
},
]);
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
index 3fd8c2a1627ab9..4aa0d8daf5b34a 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
@@ -37,10 +37,23 @@ export function initPatchCasesApi({ caseService, router }: RouteDeps) {
client: context.core.savedObjects.client,
caseIds: query.cases.map(q => q.id),
});
+ let nonExistingCases: CasePatchRequest[] = [];
const conflictedCases = query.cases.filter(q => {
const myCase = myCases.saved_objects.find(c => c.id === q.id);
+
+ if (myCase && myCase.error) {
+ nonExistingCases = [...nonExistingCases, q];
+ return false;
+ }
return myCase == null || myCase?.version !== q.version;
});
+ if (nonExistingCases.length > 0) {
+ throw Boom.notFound(
+ `These cases ${nonExistingCases
+ .map(c => c.id)
+ .join(', ')} do not exist. Please check you have the correct ids.`
+ );
+ }
if (conflictedCases.length > 0) {
throw Boom.conflict(
`These cases ${conflictedCases
@@ -60,18 +73,31 @@ export function initPatchCasesApi({ caseService, router }: RouteDeps) {
});
if (updateFilterCases.length > 0) {
const updatedBy = await caseService.getUser({ request, response });
- const { full_name, username } = updatedBy;
+ const { email, full_name, username } = updatedBy;
const updatedDt = new Date().toISOString();
const updatedCases = await caseService.patchCases({
client: context.core.savedObjects.client,
cases: updateFilterCases.map(thisCase => {
const { id: caseId, version, ...updateCaseAttributes } = thisCase;
+ let closedInfo = {};
+ if (updateCaseAttributes.status && updateCaseAttributes.status === 'closed') {
+ closedInfo = {
+ closed_at: updatedDt,
+ closed_by: { email, full_name, username },
+ };
+ } else if (updateCaseAttributes.status && updateCaseAttributes.status === 'open') {
+ closedInfo = {
+ closed_at: null,
+ closed_by: null,
+ };
+ }
return {
caseId,
updatedAttributes: {
...updateCaseAttributes,
+ ...closedInfo,
updated_at: updatedDt,
- updated_by: { full_name, username },
+ updated_by: { email, full_name, username },
},
version,
};
diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts
index eac259cc69c5a6..7af3e7b70d96fa 100644
--- a/x-pack/plugins/case/server/routes/api/types.ts
+++ b/x-pack/plugins/case/server/routes/api/types.ts
@@ -14,7 +14,7 @@ export interface RouteDeps {
}
export enum SortFieldCase {
+ closedAt = 'closed_at',
createdAt = 'created_at',
status = 'status',
- updatedAt = 'updated_at',
}
diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts
index 27ee6fc58e20a5..19dbb024d1e0be 100644
--- a/x-pack/plugins/case/server/routes/api/utils.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.ts
@@ -26,18 +26,22 @@ import { SortFieldCase } from './types';
export const transformNewCase = ({
createdDate,
- newCase,
+ email,
full_name,
+ newCase,
username,
}: {
createdDate: string;
- newCase: CaseRequest;
+ email?: string;
full_name?: string;
+ newCase: CaseRequest;
username: string;
}): CaseAttributes => ({
+ closed_at: newCase.status === 'closed' ? createdDate : null,
+ closed_by: newCase.status === 'closed' ? { email, full_name, username } : null,
comment_ids: [],
created_at: createdDate,
- created_by: { full_name, username },
+ created_by: { email, full_name, username },
updated_at: null,
updated_by: null,
...newCase,
@@ -46,18 +50,20 @@ export const transformNewCase = ({
interface NewCommentArgs {
comment: string;
createdDate: string;
+ email?: string;
full_name?: string;
username: string;
}
export const transformNewComment = ({
comment,
createdDate,
+ email,
full_name,
username,
}: NewCommentArgs): CommentAttributes => ({
comment,
created_at: createdDate,
- created_by: { full_name, username },
+ created_by: { email, full_name, username },
updated_at: null,
updated_by: null,
});
@@ -133,9 +139,9 @@ export const sortToSnake = (sortField: string): SortFieldCase => {
case 'createdAt':
case 'created_at':
return SortFieldCase.createdAt;
- case 'updatedAt':
- case 'updated_at':
- return SortFieldCase.updatedAt;
+ case 'closedAt':
+ case 'closed_at':
+ return SortFieldCase.closedAt;
default:
return SortFieldCase.createdAt;
}
diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts
index 2aa64528739b10..8eab040b9ca9cd 100644
--- a/x-pack/plugins/case/server/saved_object_types/cases.ts
+++ b/x-pack/plugins/case/server/saved_object_types/cases.ts
@@ -14,6 +14,22 @@ export const caseSavedObjectType: SavedObjectsType = {
namespaceAgnostic: false,
mappings: {
properties: {
+ closed_at: {
+ type: 'date',
+ },
+ closed_by: {
+ properties: {
+ username: {
+ type: 'keyword',
+ },
+ full_name: {
+ type: 'keyword',
+ },
+ email: {
+ type: 'keyword',
+ },
+ },
+ },
comment_ids: {
type: 'keyword',
},
@@ -28,6 +44,9 @@ export const caseSavedObjectType: SavedObjectsType = {
full_name: {
type: 'keyword',
},
+ email: {
+ type: 'keyword',
+ },
},
},
description: {
@@ -53,6 +72,9 @@ export const caseSavedObjectType: SavedObjectsType = {
full_name: {
type: 'keyword',
},
+ email: {
+ type: 'keyword',
+ },
},
},
},
diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts
index 51c31421fec2fd..f52da886e7611b 100644
--- a/x-pack/plugins/case/server/saved_object_types/comments.ts
+++ b/x-pack/plugins/case/server/saved_object_types/comments.ts
@@ -28,6 +28,9 @@ export const caseCommentSavedObjectType: SavedObjectsType = {
username: {
type: 'keyword',
},
+ email: {
+ type: 'keyword',
+ },
},
},
updated_at: {
@@ -41,6 +44,9 @@ export const caseCommentSavedObjectType: SavedObjectsType = {
full_name: {
type: 'keyword',
},
+ email: {
+ type: 'keyword',
+ },
},
},
},