[SIEM] [Cases] Case closed and add user email (#60463)
This commit is contained in:
parent
fe4c164681
commit
d5ed93ee63
|
@ -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 {
|
||||
|
|
|
@ -49,6 +49,8 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
|
|||
};
|
||||
const initialData: Case = {
|
||||
id: '',
|
||||
closedAt: null,
|
||||
closedBy: null,
|
||||
createdAt: '',
|
||||
comments: [],
|
||||
commentIds: [],
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -36,7 +36,8 @@ const Spacer = styled.span`
|
|||
const renderStringField = (field: string, dataTestSubj: string) =>
|
||||
field != null ? <span data-test-subj={dataTestSubj}>{field}</span> : getEmptyTagValue();
|
||||
export const getCasesColumns = (
|
||||
actions: Array<DefaultItemIconButtonAction<Case>>
|
||||
actions: Array<DefaultItemIconButtonAction<Case>>,
|
||||
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 (
|
||||
<FormattedRelativePreferenceDate
|
||||
value={createdAt}
|
||||
data-test-subj={`case-table-column-createdAt`}
|
||||
/>
|
||||
);
|
||||
filterStatus === 'open'
|
||||
? {
|
||||
field: 'createdAt',
|
||||
name: i18n.OPENED_ON,
|
||||
sortable: true,
|
||||
render: (createdAt: Case['createdAt']) => {
|
||||
if (createdAt != null) {
|
||||
return (
|
||||
<FormattedRelativePreferenceDate
|
||||
value={createdAt}
|
||||
data-test-subj={`case-table-column-createdAt`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return getEmptyTagValue();
|
||||
},
|
||||
}
|
||||
return getEmptyTagValue();
|
||||
},
|
||||
},
|
||||
: {
|
||||
field: 'closedAt',
|
||||
name: i18n.CLOSED_ON,
|
||||
sortable: true,
|
||||
render: (closedAt: Case['closedAt']) => {
|
||||
if (closedAt != null) {
|
||||
return (
|
||||
<FormattedRelativePreferenceDate
|
||||
value={closedAt}
|
||||
data-test-subj={`case-table-column-closedAt`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return getEmptyTagValue();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions,
|
||||
|
|
|
@ -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<FilterOptions>) => {
|
||||
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,
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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<CaseStatusProps> = ({
|
||||
'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 (
|
||||
<EuiFlexGroup gutterSize="l" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyDescriptionList compressed>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionListTitle>{i18n.STATUS}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiBadge color={badgeColor} data-test-subj="case-view-status">
|
||||
{status}
|
||||
</EuiBadge>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>{title}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedRelativePreferenceDate data-test-subj={dataTestSubj} value={value} />
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</MyDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="l" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonToggle
|
||||
data-test-subj="toggle-case-status"
|
||||
iconType={icon}
|
||||
isLoading={isLoading}
|
||||
isSelected={isSelected}
|
||||
label={buttonLabel}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<CaseViewActions caseId={caseId} caseTitle={caseTitle} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const CaseStatus = React.memo(CaseStatusComp);
|
|
@ -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 caseClosedProps: CaseProps = {
|
||||
...caseProps,
|
||||
initialData: {
|
||||
...caseProps.initialData,
|
||||
closedAt: '2020-02-20T23:06:33.798Z',
|
||||
closedBy: {
|
||||
username: 'elastic',
|
||||
},
|
||||
status: 'closed',
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
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',
|
||||
},
|
||||
version: 'WzQ3LDFd',
|
||||
...caseProps.initialData,
|
||||
};
|
||||
|
||||
export const dataClosed: Case = {
|
||||
...caseClosedProps.initialData,
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
<TestProviders>
|
||||
<CaseViewActions caseTitle={caseTitle} caseId={caseId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestProviders>
|
||||
<CaseViewActions caseTitle={caseTitle} caseId={caseId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
|
@ -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<CaseViewActions> = ({ caseId, caseTitle }) => {
|
||||
// Delete case
|
||||
const {
|
||||
handleToggleModal,
|
||||
handleOnDeleteConfirm,
|
||||
isDeleted,
|
||||
isDisplayConfirmDeleteModal,
|
||||
} = useDeleteCases();
|
||||
|
||||
const confirmDeleteModal = useMemo(
|
||||
() => (
|
||||
<ConfirmDeleteCaseModal
|
||||
caseTitle={caseTitle}
|
||||
isModalVisible={isDisplayConfirmDeleteModal}
|
||||
isPlural={false}
|
||||
onCancel={handleToggleModal}
|
||||
onConfirm={handleOnDeleteConfirm.bind(null, [caseId])}
|
||||
/>
|
||||
),
|
||||
[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 <Redirect to={`/${SiemPageName.case}`} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PropertyActions propertyActions={propertyActions} />
|
||||
{confirmDeleteModal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CaseViewActions = React.memo(CaseViewActionsComponent);
|
|
@ -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(
|
||||
<TestProviders>
|
||||
<CaseComponent {...caseClosedProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<CaseComponent {...caseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestProviders>
|
||||
<CaseComponent {...caseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<CaseProps>(({ 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<CaseProps>(({ caseId, initialData }) =>
|
|||
return null;
|
||||
}
|
||||
},
|
||||
[updateCaseProperty, caseData.status]
|
||||
[caseData.status]
|
||||
);
|
||||
const toggleStatusCase = useCallback(
|
||||
e => onUpdateField('status', e.target.checked ? 'open' : 'closed'),
|
||||
[onUpdateField]
|
||||
);
|
||||
const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]);
|
||||
const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]);
|
||||
const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]);
|
||||
const toggleStatusCase = useCallback(status => onUpdateField('status', status), [onUpdateField]);
|
||||
|
||||
// Delete case
|
||||
const {
|
||||
handleToggleModal,
|
||||
handleOnDeleteConfirm,
|
||||
isDeleted,
|
||||
isDisplayConfirmDeleteModal,
|
||||
} = useDeleteCases();
|
||||
|
||||
const confirmDeleteModal = useMemo(
|
||||
() => (
|
||||
<ConfirmDeleteCaseModal
|
||||
caseTitle={caseData.title}
|
||||
isModalVisible={isDisplayConfirmDeleteModal}
|
||||
isPlural={false}
|
||||
onCancel={handleToggleModal}
|
||||
onConfirm={handleOnDeleteConfirm.bind(null, [caseId])}
|
||||
/>
|
||||
),
|
||||
[isDisplayConfirmDeleteModal]
|
||||
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 <Redirect to={`/${SiemPageName.case}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyWrapper>
|
||||
|
@ -177,51 +143,13 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData }) =>
|
|||
}
|
||||
title={caseData.title}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="l" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyDescriptionList compressed>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionListTitle>{i18n.STATUS}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiBadge
|
||||
color={caseData.status === 'open' ? 'secondary' : 'danger'}
|
||||
data-test-subj="case-view-status"
|
||||
>
|
||||
{caseData.status}
|
||||
</EuiBadge>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>{i18n.CASE_OPENED}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedRelativePreferenceDate
|
||||
data-test-subj="case-view-createdAt"
|
||||
value={caseData.createdAt}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</MyDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="l" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonToggle
|
||||
data-test-subj="toggle-case-status"
|
||||
iconType={caseData.status === 'open' ? 'checkInCircleFilled' : 'magnet'}
|
||||
isLoading={isLoading && updateKey === 'status'}
|
||||
isSelected={caseData.status === 'open'}
|
||||
label={caseData.status === 'open' ? 'Close case' : 'Reopen case'}
|
||||
onChange={toggleStatusCase}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} data-test-subj="case-view-actions">
|
||||
<PropertyActions propertyActions={propertyActions} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<CaseStatus
|
||||
caseId={caseData.id}
|
||||
caseTitle={caseData.title}
|
||||
isLoading={isLoading && updateKey === 'status'}
|
||||
toggleStatusCase={toggleStatusCase}
|
||||
{...caseStatusData}
|
||||
/>
|
||||
</HeaderPage>
|
||||
</MyWrapper>
|
||||
<WhitePageWrapper>
|
||||
|
@ -237,6 +165,7 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData }) =>
|
|||
<EuiFlexItem grow={2}>
|
||||
<UserList
|
||||
data-test-subj="case-view-user-list"
|
||||
email={emailContent}
|
||||
headline={i18n.REPORTER}
|
||||
users={[caseData.createdBy]}
|
||||
/>
|
||||
|
@ -250,7 +179,6 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData }) =>
|
|||
</EuiFlexGroup>
|
||||
</MyWrapper>
|
||||
</WhitePageWrapper>
|
||||
{confirmDeleteModal}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -273,4 +201,5 @@ export const CaseView = React.memo(({ caseId }: Props) => {
|
|||
return <CaseComponent caseId={caseId} initialData={data} />;
|
||||
});
|
||||
|
||||
CaseComponent.displayName = 'CaseComponent';
|
||||
CaseView.displayName = 'CaseView';
|
||||
|
|
|
@ -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}',
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
<UserList
|
||||
email={{
|
||||
subject: i18n.EMAIL_SUBJECT(title),
|
||||
body: i18n.EMAIL_BODY(caseLink),
|
||||
}}
|
||||
headline={i18n.REPORTER}
|
||||
users={[user]}
|
||||
/>
|
||||
);
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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) => (
|
||||
<MyFlexGroup key={key} justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
|
@ -50,7 +57,8 @@ const renderUsers = (users: ElasticUser[]) => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
onClick={() => {}} // 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 (
|
||||
<EuiText>
|
||||
<h4>{headline}</h4>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
{renderUsers(users)}
|
||||
{renderUsers(users, handleSendEmail)}
|
||||
</EuiText>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -12,10 +12,13 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
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<SavedObject<CaseAttributes>> = [
|
|||
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<SavedObject<CaseAttributes>> = [
|
|||
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<SavedObject<CaseAttributes>> = [
|
|||
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<SavedObject<CaseAttributes>> = [
|
|||
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<SavedObject<CaseAttributes>> = [
|
|||
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<SavedObject<CommentAttributes>> = [
|
|||
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<SavedObject<CommentAttributes>> = [
|
|||
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<SavedObject<CommentAttributes>> = [
|
|||
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',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ export interface RouteDeps {
|
|||
}
|
||||
|
||||
export enum SortFieldCase {
|
||||
closedAt = 'closed_at',
|
||||
createdAt = 'created_at',
|
||||
status = 'status',
|
||||
updatedAt = 'updated_at',
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue