[Security Solution][Case] Manual attach alert to a case (#82996)
This commit is contained in:
parent
4b4419a930
commit
11470ac23a
|
@ -77,7 +77,7 @@ describe('AddComment ', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(onCommentSaving).toBeCalled();
|
||||
expect(postComment).toBeCalledWith(sampleData, onCommentPosted);
|
||||
expect(postComment).toBeCalledWith(addCommentProps.caseId, sampleData, onCommentPosted);
|
||||
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -43,7 +43,7 @@ interface AddCommentProps {
|
|||
export const AddComment = React.memo(
|
||||
forwardRef<AddCommentRefObject, AddCommentProps>(
|
||||
({ caseId, disabled, showLoading = true, onCommentPosted, onCommentSaving }, ref) => {
|
||||
const { isLoading, postComment } = usePostComment(caseId);
|
||||
const { isLoading, postComment } = usePostComment();
|
||||
|
||||
const { form } = useForm<AddCommentFormSchema>({
|
||||
defaultValue: initialCommentValue,
|
||||
|
@ -79,10 +79,10 @@ export const AddComment = React.memo(
|
|||
if (onCommentSaving != null) {
|
||||
onCommentSaving();
|
||||
}
|
||||
postComment({ ...data, type: CommentType.user }, onCommentPosted);
|
||||
postComment(caseId, { ...data, type: CommentType.user }, onCommentPosted);
|
||||
reset();
|
||||
}
|
||||
}, [onCommentPosted, onCommentSaving, postComment, reset, submit]);
|
||||
}, [onCommentPosted, onCommentSaving, postComment, reset, submit, caseId]);
|
||||
|
||||
return (
|
||||
<span id="add-comment-permLink">
|
||||
|
|
|
@ -437,7 +437,44 @@ describe('AllCases', () => {
|
|||
);
|
||||
await waitFor(() => {
|
||||
wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click');
|
||||
expect(onRowClick).toHaveBeenCalledWith('1');
|
||||
expect(onRowClick).toHaveBeenCalledWith({
|
||||
closedAt: null,
|
||||
closedBy: null,
|
||||
comments: [],
|
||||
connector: { fields: null, id: '123', name: 'My Connector', type: '.none' },
|
||||
createdAt: '2020-02-19T23:06:33.798Z',
|
||||
createdBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
description: 'Security banana Issue',
|
||||
externalService: {
|
||||
connectorId: '123',
|
||||
connectorName: 'connector name',
|
||||
externalId: 'external_id',
|
||||
externalTitle: 'external title',
|
||||
externalUrl: 'basicPush.com',
|
||||
pushedAt: '2020-02-20T15:02:57.995Z',
|
||||
pushedBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
},
|
||||
id: '1',
|
||||
status: 'open',
|
||||
tags: ['coke', 'pepsi'],
|
||||
title: 'Another horrible breach!!',
|
||||
totalComment: 0,
|
||||
updatedAt: '2020-02-20T15:02:57.995Z',
|
||||
updatedBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
version: 'WzQ3LDFd',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ const getSortField = (field: string): SortFieldCase => {
|
|||
};
|
||||
|
||||
interface AllCasesProps {
|
||||
onRowClick?: (id?: string) => void;
|
||||
onRowClick?: (theCase?: Case) => void;
|
||||
isModal?: boolean;
|
||||
userCanCrud: boolean;
|
||||
}
|
||||
|
@ -339,32 +339,20 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
|
||||
const TableWrap = useMemo(() => (isModal ? 'span' : Panel), [isModal]);
|
||||
|
||||
const onTableRowClick = useMemo(
|
||||
() =>
|
||||
memoize<(id: string) => () => void>((id) => () => {
|
||||
if (onRowClick) {
|
||||
onRowClick(id);
|
||||
}
|
||||
}),
|
||||
[onRowClick]
|
||||
);
|
||||
|
||||
const tableRowProps = useCallback(
|
||||
(item) => {
|
||||
const rowProps = {
|
||||
'data-test-subj': `cases-table-row-${item.id}`,
|
||||
(theCase: Case) => {
|
||||
const onTableRowClick = memoize(() => {
|
||||
if (onRowClick) {
|
||||
onRowClick(theCase);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
'data-test-subj': `cases-table-row-${theCase.id}`,
|
||||
...(isModal ? { onClick: onTableRowClick } : {}),
|
||||
};
|
||||
|
||||
if (isModal) {
|
||||
return {
|
||||
...rowProps,
|
||||
onClick: onTableRowClick(item.id),
|
||||
};
|
||||
}
|
||||
|
||||
return rowProps;
|
||||
},
|
||||
[isModal, onTableRowClick]
|
||||
[isModal, onRowClick]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import '../../../common/mock/match_media';
|
||||
import { AllCasesModal } from '.';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
import { useGetCasesMockState, basicCaseId } from '../../containers/mock';
|
||||
import { useDeleteCases } from '../../containers/use_delete_cases';
|
||||
import { useGetCases } from '../../containers/use_get_cases';
|
||||
import { useGetCasesStatus } from '../../containers/use_get_cases_status';
|
||||
import { useUpdateCases } from '../../containers/use_bulk_update_case';
|
||||
import { EuiTableRow } from '@elastic/eui';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useHistory: () => ({
|
||||
useHistory: jest.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/components/link_to');
|
||||
|
||||
jest.mock('../../containers/use_bulk_update_case');
|
||||
jest.mock('../../containers/use_delete_cases');
|
||||
jest.mock('../../containers/use_get_cases');
|
||||
jest.mock('../../containers/use_get_cases_status');
|
||||
|
||||
const useDeleteCasesMock = useDeleteCases as jest.Mock;
|
||||
const useGetCasesMock = useGetCases as jest.Mock;
|
||||
const useGetCasesStatusMock = useGetCasesStatus as jest.Mock;
|
||||
const useUpdateCasesMock = useUpdateCases as jest.Mock;
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useGetUserSavedObjectPermissions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const onCloseCaseModal = jest.fn();
|
||||
const onRowClick = jest.fn();
|
||||
const defaultProps = {
|
||||
onCloseCaseModal,
|
||||
onRowClick,
|
||||
showCaseModal: true,
|
||||
};
|
||||
describe('AllCasesModal', () => {
|
||||
const dispatchResetIsDeleted = jest.fn();
|
||||
const dispatchResetIsUpdated = jest.fn();
|
||||
const dispatchUpdateCaseProperty = jest.fn();
|
||||
const handleOnDeleteConfirm = jest.fn();
|
||||
const handleToggleModal = jest.fn();
|
||||
const refetchCases = jest.fn();
|
||||
const setFilters = jest.fn();
|
||||
const setQueryParams = jest.fn();
|
||||
const setSelectedCases = jest.fn();
|
||||
const updateBulkStatus = jest.fn();
|
||||
const fetchCasesStatus = jest.fn();
|
||||
|
||||
const defaultGetCases = {
|
||||
...useGetCasesMockState,
|
||||
dispatchUpdateCaseProperty,
|
||||
refetchCases,
|
||||
setFilters,
|
||||
setQueryParams,
|
||||
setSelectedCases,
|
||||
};
|
||||
const defaultDeleteCases = {
|
||||
dispatchResetIsDeleted,
|
||||
handleOnDeleteConfirm,
|
||||
handleToggleModal,
|
||||
isDeleted: false,
|
||||
isDisplayConfirmDeleteModal: false,
|
||||
isLoading: false,
|
||||
};
|
||||
const defaultCasesStatus = {
|
||||
countClosedCases: 0,
|
||||
countOpenCases: 5,
|
||||
fetchCasesStatus,
|
||||
isError: false,
|
||||
isLoading: true,
|
||||
};
|
||||
const defaultUpdateCases = {
|
||||
isUpdated: false,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
dispatchResetIsUpdated,
|
||||
updateBulkStatus,
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useUpdateCasesMock.mockImplementation(() => defaultUpdateCases);
|
||||
useGetCasesMock.mockImplementation(() => defaultGetCases);
|
||||
useDeleteCasesMock.mockImplementation(() => defaultDeleteCases);
|
||||
useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus);
|
||||
});
|
||||
|
||||
it('renders with unselectable rows', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesModal {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(EuiTableRow).first().prop('isSelectable')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
it('does not render modal if showCaseModal: false', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesModal {...{ ...defaultProps, showCaseModal: false }} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
it('onRowClick called when row is clicked', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesModal {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
const firstRow = wrapper.find(EuiTableRow).first();
|
||||
firstRow.simulate('click');
|
||||
expect(onRowClick.mock.calls[0][0]).toEqual(basicCaseId);
|
||||
});
|
||||
});
|
||||
it('Closing modal calls onCloseCaseModal', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesModal {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
const modalClose = wrapper.find('.euiModal__closeIcon').first();
|
||||
modalClose.simulate('click');
|
||||
expect(onCloseCaseModal).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana';
|
||||
import { AllCases } from '../all_cases';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface AllCasesModalProps {
|
||||
onCloseCaseModal: () => void;
|
||||
showCaseModal: boolean;
|
||||
onRowClick: (id?: string) => void;
|
||||
}
|
||||
|
||||
export const AllCasesModalComponent = ({
|
||||
onCloseCaseModal,
|
||||
onRowClick,
|
||||
showCaseModal,
|
||||
}: AllCasesModalProps) => {
|
||||
const userPermissions = useGetUserSavedObjectPermissions();
|
||||
let modal;
|
||||
if (showCaseModal) {
|
||||
modal = (
|
||||
<EuiOverlayMask data-test-subj="all-cases-modal">
|
||||
<EuiModal onClose={onCloseCaseModal}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.SELECT_CASE_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<AllCases
|
||||
onRowClick={onRowClick}
|
||||
userCanCrud={userPermissions?.crud ?? false}
|
||||
isModal
|
||||
/>
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{modal}</>;
|
||||
};
|
||||
|
||||
export const AllCasesModal = React.memo(AllCasesModalComponent);
|
||||
|
||||
AllCasesModal.displayName = 'AllCasesModal';
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
export const SELECT_CASE_TITLE = i18n.translate('xpack.securitySolution.case.caseModal.title', {
|
||||
defaultMessage: 'Select case to attach timeline',
|
||||
});
|
|
@ -19,7 +19,7 @@ const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, sel
|
|||
|
||||
const onCaseCreated = useCallback(() => refetchCases(), [refetchCases]);
|
||||
|
||||
const { Modal: CreateCaseModal, openModal } = useCreateCaseModal({ onCaseCreated });
|
||||
const { modal, openModal } = useCreateCaseModal({ onCaseCreated });
|
||||
|
||||
const onChange = useCallback(
|
||||
(id: string) => {
|
||||
|
@ -46,7 +46,7 @@ const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, sel
|
|||
selectedCase={selectedCase ?? undefined}
|
||||
onCaseChanged={onChange}
|
||||
/>
|
||||
<CreateCaseModal />
|
||||
{modal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* 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, { memo, useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuPanel,
|
||||
EuiText,
|
||||
EuiContextMenuItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CommentType } from '../../../../../case/common/api';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item';
|
||||
import * as i18n from './translations';
|
||||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { Case } from '../../containers/types';
|
||||
import { displaySuccessToast, useStateToaster } from '../../../common/components/toasters';
|
||||
import { useCreateCaseModal } from '../use_create_case_modal';
|
||||
import { useAllCasesModal } from '../use_all_cases_modal';
|
||||
|
||||
interface AddToCaseActionProps {
|
||||
ecsRowData: Ecs;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({ ecsRowData, disabled }) => {
|
||||
const eventId = ecsRowData._id;
|
||||
const eventIndex = ecsRowData._index;
|
||||
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const openPopover = useCallback(() => setIsPopoverOpen(true), []);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
|
||||
const { postComment } = usePostComment();
|
||||
const attachAlertToCase = useCallback(
|
||||
(theCase: Case) => {
|
||||
postComment(
|
||||
theCase.id,
|
||||
{
|
||||
type: CommentType.alert,
|
||||
alertId: eventId,
|
||||
index: eventIndex ?? '',
|
||||
},
|
||||
() => displaySuccessToast(i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title), dispatchToaster)
|
||||
);
|
||||
},
|
||||
[postComment, eventId, eventIndex, dispatchToaster]
|
||||
);
|
||||
|
||||
const { modal: createCaseModal, openModal: openCreateCaseModal } = useCreateCaseModal({
|
||||
onCaseCreated: attachAlertToCase,
|
||||
});
|
||||
|
||||
const onCaseClicked = useCallback(
|
||||
(theCase) => {
|
||||
/**
|
||||
* No cases listed on the table.
|
||||
* The user pressed the add new case table's button.
|
||||
* We gonna open the create case modal.
|
||||
*/
|
||||
if (theCase == null) {
|
||||
openCreateCaseModal();
|
||||
return;
|
||||
}
|
||||
|
||||
attachAlertToCase(theCase);
|
||||
},
|
||||
[attachAlertToCase, openCreateCaseModal]
|
||||
);
|
||||
|
||||
const { modal: allCasesModal, openModal: openAllCaseModal } = useAllCasesModal({
|
||||
onRowClick: onCaseClicked,
|
||||
});
|
||||
|
||||
const addNewCaseClick = useCallback(() => {
|
||||
closePopover();
|
||||
openCreateCaseModal();
|
||||
}, [openCreateCaseModal, closePopover]);
|
||||
|
||||
const addExistingCaseClick = useCallback(() => {
|
||||
closePopover();
|
||||
openAllCaseModal();
|
||||
}, [openAllCaseModal, closePopover]);
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
<EuiContextMenuItem
|
||||
key="add-new-case-menu-item"
|
||||
onClick={addNewCaseClick}
|
||||
aria-label={i18n.ACTION_ADD_NEW_CASE}
|
||||
data-test-subj="add-new-case-item"
|
||||
disabled={disabled}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_ADD_NEW_CASE}</EuiText>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="add-existing-case-menu-item"
|
||||
onClick={addExistingCaseClick}
|
||||
aria-label={i18n.ACTION_ADD_EXISTING_CASE}
|
||||
data-test-subj="add-existing-case-menu-item"
|
||||
disabled={disabled}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_ADD_EXISTING_CASE}</EuiText>
|
||||
</EuiContextMenuItem>,
|
||||
],
|
||||
[addExistingCaseClick, addNewCaseClick, disabled]
|
||||
);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiToolTip
|
||||
data-test-subj="attach-alert-to-case-tooltip"
|
||||
content={i18n.ACTION_ADD_TO_CASE_TOOLTIP}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.ACTION_ADD_TO_CASE_ARIA_LABEL}
|
||||
data-test-subj="attach-alert-to-case-button"
|
||||
size="s"
|
||||
iconType="folderClosed"
|
||||
onClick={openPopover}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
),
|
||||
[disabled, openPopover]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIconItem id="attachAlertToCase">
|
||||
<EuiPopover
|
||||
id="attachAlertToCasePanel"
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenuPanel items={items} />
|
||||
</EuiPopover>
|
||||
</ActionIconItem>
|
||||
{createCaseModal}
|
||||
{allCasesModal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddToCaseAction = memo(AddToCaseActionComponent);
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ACTION_ADD_CASE = i18n.translate(
|
||||
'xpack.securitySolution.case.timeline.actions.addCase',
|
||||
{
|
||||
defaultMessage: 'Add to case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.case.timeline.actions.addNewCase',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_EXISTING_CASE = i18n.translate(
|
||||
'xpack.securitySolution.case.timeline.actions.addExistingCase',
|
||||
{
|
||||
defaultMessage: 'Add to existing case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_TO_CASE_ARIA_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.timeline.actions.addToCaseAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Attach alert to case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_TO_CASE_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.case.timeline.actions.addToCaseTooltip',
|
||||
{
|
||||
defaultMessage: 'Add to case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CREATED_SUCCESS_TOAST = (title: string) =>
|
||||
i18n.translate('xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToast', {
|
||||
values: { title },
|
||||
defaultMessage: 'An alert has been added to "{title}"',
|
||||
});
|
|
@ -3,6 +3,8 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import '../../../common/mock/match_media';
|
||||
|
@ -10,10 +12,19 @@ import { AllCasesModal } from './all_cases_modal';
|
|||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
jest.mock('../all_cases', () => {
|
||||
const AllCases = () => {
|
||||
return <></>;
|
||||
return {
|
||||
AllCases: ({ onRowClick }: { onRowClick: ({ id }: { id: string }) => void }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="all-cases-row"
|
||||
onClick={() => onRowClick({ id: 'case-id' })}
|
||||
>
|
||||
{'case-row'}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
};
|
||||
return { AllCases };
|
||||
});
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
|
@ -27,6 +38,7 @@ jest.mock('../../../common/lib/kibana', () => {
|
|||
const onCloseCaseModal = jest.fn();
|
||||
const onRowClick = jest.fn();
|
||||
const defaultProps = {
|
||||
isModalOpen: true,
|
||||
onCloseCaseModal,
|
||||
onRowClick,
|
||||
};
|
||||
|
@ -46,6 +58,16 @@ describe('AllCasesModal', () => {
|
|||
expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it does not render the modal isModalOpen=false ', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesModal {...defaultProps} isModalOpen={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('Closing modal calls onCloseCaseModal', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
@ -71,4 +93,15 @@ describe('AllCasesModal', () => {
|
|||
isModal: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('onRowClick called when row is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesModal {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj='all-cases-row']`).first().simulate('click');
|
||||
expect(onRowClick).toHaveBeenCalledWith({ id: 'case-id' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,18 +14,25 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana';
|
||||
import { Case } from '../../containers/types';
|
||||
import { AllCases } from '../all_cases';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface AllCasesModalProps {
|
||||
isModalOpen: boolean;
|
||||
onCloseCaseModal: () => void;
|
||||
onRowClick: (id?: string) => void;
|
||||
onRowClick: (theCase?: Case) => void;
|
||||
}
|
||||
|
||||
const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({ onCloseCaseModal, onRowClick }) => {
|
||||
const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({
|
||||
isModalOpen,
|
||||
onCloseCaseModal,
|
||||
onRowClick,
|
||||
}) => {
|
||||
const userPermissions = useGetUserSavedObjectPermissions();
|
||||
const userCanCrud = userPermissions?.crud ?? false;
|
||||
return (
|
||||
|
||||
return isModalOpen ? (
|
||||
<EuiOverlayMask data-test-subj="all-cases-modal">
|
||||
<EuiModal onClose={onCloseCaseModal}>
|
||||
<EuiModalHeader>
|
||||
|
@ -36,7 +43,7 @@ const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({ onCloseCaseModal
|
|||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const AllCasesModal = memo(AllCasesModalComponent);
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
*/
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import '../../../common/mock/match_media';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { useAllCasesModal, UseAllCasesModalProps, UseAllCasesModalReturnedValues } from '.';
|
||||
import { mockTimelineModel, TestProviders } from '../../../common/mock';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
|
@ -26,10 +26,22 @@ jest.mock('react-redux', () => {
|
|||
});
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../all_cases', () => {
|
||||
return {
|
||||
AllCases: ({ onRowClick }: { onRowClick: ({ id }: { id: string }) => void }) => {
|
||||
return (
|
||||
<button type="button" onClick={() => onRowClick({ id: 'case-id' })}>
|
||||
{'case-row'}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/hooks/use_selector');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
const onRowClick = jest.fn();
|
||||
|
||||
describe('useAllCasesModal', () => {
|
||||
let navigateToApp: jest.Mock;
|
||||
|
@ -42,51 +54,51 @@ describe('useAllCasesModal', () => {
|
|||
|
||||
it('init', async () => {
|
||||
const { result } = renderHook<UseAllCasesModalProps, UseAllCasesModalReturnedValues>(
|
||||
() => useAllCasesModal({ timelineId: TimelineId.test }),
|
||||
() => useAllCasesModal({ onRowClick }),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.current.showModal).toBe(false);
|
||||
expect(result.current.isModalOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('opens the modal', async () => {
|
||||
const { result } = renderHook<UseAllCasesModalProps, UseAllCasesModalReturnedValues>(
|
||||
() => useAllCasesModal({ timelineId: TimelineId.test }),
|
||||
() => useAllCasesModal({ onRowClick }),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.onOpenModal();
|
||||
result.current.openModal();
|
||||
});
|
||||
|
||||
expect(result.current.showModal).toBe(true);
|
||||
expect(result.current.isModalOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('closes the modal', async () => {
|
||||
const { result } = renderHook<UseAllCasesModalProps, UseAllCasesModalReturnedValues>(
|
||||
() => useAllCasesModal({ timelineId: TimelineId.test }),
|
||||
() => useAllCasesModal({ onRowClick }),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.onOpenModal();
|
||||
result.current.onCloseModal();
|
||||
result.current.openModal();
|
||||
result.current.closeModal();
|
||||
});
|
||||
|
||||
expect(result.current.showModal).toBe(false);
|
||||
expect(result.current.isModalOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('returns a memoized value', async () => {
|
||||
const { result, rerender } = renderHook<UseAllCasesModalProps, UseAllCasesModalReturnedValues>(
|
||||
() => useAllCasesModal({ timelineId: TimelineId.test }),
|
||||
() => useAllCasesModal({ onRowClick }),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -99,49 +111,24 @@ describe('useAllCasesModal', () => {
|
|||
|
||||
it('closes the modal when clicking a row', async () => {
|
||||
const { result } = renderHook<UseAllCasesModalProps, UseAllCasesModalReturnedValues>(
|
||||
() => useAllCasesModal({ timelineId: TimelineId.test }),
|
||||
() => useAllCasesModal({ onRowClick }),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.onOpenModal();
|
||||
result.current.onRowClick();
|
||||
result.current.openModal();
|
||||
});
|
||||
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
it('navigates to the correct path without id', async () => {
|
||||
const { result } = renderHook<UseAllCasesModalProps, UseAllCasesModalReturnedValues>(
|
||||
() => useAllCasesModal({ timelineId: TimelineId.test }),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
const modal = result.current.modal;
|
||||
render(<>{modal}</>);
|
||||
|
||||
act(() => {
|
||||
result.current.onOpenModal();
|
||||
result.current.onRowClick();
|
||||
userEvent.click(screen.getByText('case-row'));
|
||||
});
|
||||
|
||||
expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' });
|
||||
});
|
||||
|
||||
it('navigates to the correct path with id', async () => {
|
||||
const { result } = renderHook<UseAllCasesModalProps, UseAllCasesModalReturnedValues>(
|
||||
() => useAllCasesModal({ timelineId: TimelineId.test }),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.onOpenModal();
|
||||
result.current.onRowClick('case-id');
|
||||
});
|
||||
|
||||
expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/case-id' });
|
||||
expect(result.current.isModalOpen).toBe(false);
|
||||
expect(onRowClick).toHaveBeenCalledWith({ id: 'case-id' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,84 +4,50 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pick } from 'lodash/fp';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../common/components/link_to';
|
||||
import { setInsertTimeline } from '../../../timelines/store/timeline/actions';
|
||||
import { timelineSelectors } from '../../../timelines/store/timeline';
|
||||
|
||||
import { Case } from '../../containers/types';
|
||||
import { AllCasesModal } from './all_cases_modal';
|
||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
|
||||
export interface UseAllCasesModalProps {
|
||||
timelineId: string;
|
||||
onRowClick: (theCase?: Case) => void;
|
||||
}
|
||||
|
||||
export interface UseAllCasesModalReturnedValues {
|
||||
Modal: React.FC;
|
||||
showModal: boolean;
|
||||
onCloseModal: () => void;
|
||||
onOpenModal: () => void;
|
||||
onRowClick: (id?: string) => void;
|
||||
modal: JSX.Element;
|
||||
isModalOpen: boolean;
|
||||
closeModal: () => void;
|
||||
openModal: () => void;
|
||||
}
|
||||
|
||||
export const useAllCasesModal = ({
|
||||
timelineId,
|
||||
onRowClick,
|
||||
}: UseAllCasesModalProps): UseAllCasesModalReturnedValues => {
|
||||
const dispatch = useDispatch();
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const { graphEventId, savedObjectId, title } = useDeepEqualSelector((state) =>
|
||||
pick(
|
||||
['graphEventId', 'savedObjectId', 'title'],
|
||||
timelineSelectors.selectTimeline(state, timelineId) ?? timelineDefaults
|
||||
)
|
||||
);
|
||||
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
const onCloseModal = useCallback(() => setShowModal(false), []);
|
||||
const onOpenModal = useCallback(() => setShowModal(true), []);
|
||||
|
||||
const onRowClick = useCallback(
|
||||
async (id?: string) => {
|
||||
onCloseModal();
|
||||
|
||||
await navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
|
||||
path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(),
|
||||
});
|
||||
|
||||
dispatch(
|
||||
setInsertTimeline({
|
||||
graphEventId,
|
||||
timelineId,
|
||||
timelineSavedObjectId: savedObjectId,
|
||||
timelineTitle: title,
|
||||
})
|
||||
);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const closeModal = useCallback(() => setIsModalOpen(false), []);
|
||||
const openModal = useCallback(() => setIsModalOpen(true), []);
|
||||
const onClick = useCallback(
|
||||
(theCase?: Case) => {
|
||||
closeModal();
|
||||
onRowClick(theCase);
|
||||
},
|
||||
[onCloseModal, navigateToApp, dispatch, graphEventId, timelineId, savedObjectId, title]
|
||||
);
|
||||
|
||||
const Modal: React.FC = useCallback(
|
||||
() =>
|
||||
showModal ? <AllCasesModal onCloseCaseModal={onCloseModal} onRowClick={onRowClick} /> : null,
|
||||
[onCloseModal, onRowClick, showModal]
|
||||
[closeModal, onRowClick]
|
||||
);
|
||||
|
||||
const state = useMemo(
|
||||
() => ({
|
||||
Modal,
|
||||
showModal,
|
||||
onCloseModal,
|
||||
onOpenModal,
|
||||
modal: (
|
||||
<AllCasesModal
|
||||
isModalOpen={isModalOpen}
|
||||
onCloseCaseModal={closeModal}
|
||||
onRowClick={onClick}
|
||||
/>
|
||||
),
|
||||
isModalOpen,
|
||||
closeModal,
|
||||
openModal,
|
||||
onRowClick,
|
||||
}),
|
||||
[showModal, onCloseModal, onOpenModal, onRowClick, Modal]
|
||||
[isModalOpen, closeModal, onClick, openModal, onRowClick]
|
||||
);
|
||||
|
||||
return state;
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
export const SELECT_CASE_TITLE = i18n.translate('xpack.securitySolution.case.caseModal.title', {
|
||||
defaultMessage: 'Select case to attach timeline',
|
||||
defaultMessage: 'Select case',
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
EuiModal,
|
||||
|
@ -21,8 +21,9 @@ import { Case } from '../../containers/types';
|
|||
import * as i18n from '../../translations';
|
||||
|
||||
export interface CreateCaseModalProps {
|
||||
isModalOpen: boolean;
|
||||
onCloseCaseModal: () => void;
|
||||
onCaseCreated: (theCase: Case) => void;
|
||||
onSuccess: (theCase: Case) => void;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
|
@ -33,18 +34,11 @@ const Container = styled.div`
|
|||
`;
|
||||
|
||||
const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
||||
isModalOpen,
|
||||
onCloseCaseModal,
|
||||
onCaseCreated,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const onSuccess = useCallback(
|
||||
(theCase) => {
|
||||
onCaseCreated(theCase);
|
||||
onCloseCaseModal();
|
||||
},
|
||||
[onCaseCreated, onCloseCaseModal]
|
||||
);
|
||||
|
||||
return (
|
||||
return isModalOpen ? (
|
||||
<EuiOverlayMask data-test-subj="all-cases-modal">
|
||||
<EuiModal onClose={onCloseCaseModal}>
|
||||
<EuiModalHeader>
|
||||
|
@ -60,7 +54,7 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
|||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const CreateCaseModal = memo(CreateModalComponent);
|
||||
|
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
onCaseCreated: (theCase: Case) => void;
|
||||
}
|
||||
export interface UseAllCasesModalReturnedValues {
|
||||
Modal: React.FC;
|
||||
modal: JSX.Element;
|
||||
isModalOpen: boolean;
|
||||
closeModal: () => void;
|
||||
openModal: () => void;
|
||||
|
@ -22,23 +22,28 @@ export const useCreateCaseModal = ({ onCaseCreated }: Props) => {
|
|||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const closeModal = useCallback(() => setIsModalOpen(false), []);
|
||||
const openModal = useCallback(() => setIsModalOpen(true), []);
|
||||
|
||||
const Modal: React.FC = useCallback(
|
||||
() =>
|
||||
isModalOpen ? (
|
||||
<CreateCaseModal onCloseCaseModal={closeModal} onCaseCreated={onCaseCreated} />
|
||||
) : null,
|
||||
[closeModal, isModalOpen, onCaseCreated]
|
||||
const onSuccess = useCallback(
|
||||
(theCase) => {
|
||||
onCaseCreated(theCase);
|
||||
closeModal();
|
||||
},
|
||||
[onCaseCreated, closeModal]
|
||||
);
|
||||
|
||||
const state = useMemo(
|
||||
() => ({
|
||||
Modal,
|
||||
modal: (
|
||||
<CreateCaseModal
|
||||
isModalOpen={isModalOpen}
|
||||
onCloseCaseModal={closeModal}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
),
|
||||
isModalOpen,
|
||||
closeModal,
|
||||
openModal,
|
||||
}),
|
||||
[isModalOpen, closeModal, openModal, Modal]
|
||||
[isModalOpen, closeModal, onSuccess, openModal]
|
||||
);
|
||||
|
||||
return state;
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
CasePatchRequest,
|
||||
CasePostRequest,
|
||||
CasesStatusResponse,
|
||||
CommentRequestUserType,
|
||||
CommentRequest,
|
||||
User,
|
||||
CaseUserActionsResponse,
|
||||
CaseExternalServiceRequest,
|
||||
|
@ -183,7 +183,7 @@ export const patchCasesStatus = async (
|
|||
};
|
||||
|
||||
export const postComment = async (
|
||||
newComment: CommentRequestUserType,
|
||||
newComment: CommentRequest,
|
||||
caseId: string,
|
||||
signal: AbortSignal
|
||||
): Promise<Case> => {
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('usePostComment', () => {
|
|||
it('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() =>
|
||||
usePostComment(basicCaseId)
|
||||
usePostComment()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
|
@ -44,11 +44,11 @@ describe('usePostComment', () => {
|
|||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() =>
|
||||
usePostComment(basicCaseId)
|
||||
usePostComment()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.postComment(samplePost, updateCaseCallback);
|
||||
result.current.postComment(basicCaseId, samplePost, updateCaseCallback);
|
||||
await waitForNextUpdate();
|
||||
expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal);
|
||||
});
|
||||
|
@ -57,10 +57,10 @@ describe('usePostComment', () => {
|
|||
it('post case', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() =>
|
||||
usePostComment(basicCaseId)
|
||||
usePostComment()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
result.current.postComment(samplePost, updateCaseCallback);
|
||||
result.current.postComment(basicCaseId, samplePost, updateCaseCallback);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
|
@ -73,10 +73,10 @@ describe('usePostComment', () => {
|
|||
it('set isLoading to true when posting case', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() =>
|
||||
usePostComment(basicCaseId)
|
||||
usePostComment()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
result.current.postComment(samplePost, updateCaseCallback);
|
||||
result.current.postComment(basicCaseId, samplePost, updateCaseCallback);
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
});
|
||||
|
@ -90,10 +90,10 @@ describe('usePostComment', () => {
|
|||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() =>
|
||||
usePostComment(basicCaseId)
|
||||
usePostComment()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
result.current.postComment(samplePost, updateCaseCallback);
|
||||
result.current.postComment(basicCaseId, samplePost, updateCaseCallback);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { useReducer, useCallback } from 'react';
|
||||
|
||||
import { CommentRequestUserType } from '../../../../case/common/api';
|
||||
import { CommentRequest } from '../../../../case/common/api';
|
||||
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
|
||||
|
||||
import { postComment } from './api';
|
||||
|
@ -42,10 +42,10 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta
|
|||
};
|
||||
|
||||
export interface UsePostComment extends NewCommentState {
|
||||
postComment: (data: CommentRequestUserType, updateCase: (newCase: Case) => void) => void;
|
||||
postComment: (caseId: string, data: CommentRequest, updateCase?: (newCase: Case) => void) => void;
|
||||
}
|
||||
|
||||
export const usePostComment = (caseId: string): UsePostComment => {
|
||||
export const usePostComment = (): UsePostComment => {
|
||||
const [state, dispatch] = useReducer(dataFetchReducer, {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
@ -53,7 +53,7 @@ export const usePostComment = (caseId: string): UsePostComment => {
|
|||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
const postMyComment = useCallback(
|
||||
async (data: CommentRequestUserType, updateCase: (newCase: Case) => void) => {
|
||||
async (caseId: string, data: CommentRequest, updateCase?: (newCase: Case) => void) => {
|
||||
let cancel = false;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
|
@ -62,7 +62,9 @@ export const usePostComment = (caseId: string): UsePostComment => {
|
|||
const response = await postComment(data, caseId, abortCtrl.signal);
|
||||
if (!cancel) {
|
||||
dispatch({ type: 'FETCH_SUCCESS' });
|
||||
updateCase(response);
|
||||
if (updateCase) {
|
||||
updateCase(response);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancel) {
|
||||
|
@ -79,8 +81,7 @@ export const usePostComment = (caseId: string): UsePostComment => {
|
|||
cancel = true;
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[caseId]
|
||||
[dispatchToaster]
|
||||
);
|
||||
|
||||
return { ...state, postComment: postMyComment };
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { useKibana } from '../../../../common/lib/kibana';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { mockTimelineModel, TestProviders } from '../../../../common/mock';
|
||||
import { useAllCasesModal } from '../../../../cases/components/use_all_cases_modal';
|
||||
import { AddToCaseButton } from '.';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/hooks/use_selector');
|
||||
jest.mock('../../../../cases/components/use_all_cases_modal');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
const useAllCasesModalMock = useAllCasesModal as jest.Mock;
|
||||
|
||||
describe('EventColumnView', () => {
|
||||
const navigateToApp = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
useKibanaMock().services.application.navigateToApp = navigateToApp;
|
||||
(useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel);
|
||||
});
|
||||
|
||||
it('navigates to the correct path without id', async () => {
|
||||
useAllCasesModalMock.mockImplementation(({ onRowClick }) => {
|
||||
onRowClick();
|
||||
|
||||
return {
|
||||
modal: <>{'test'}</>,
|
||||
openModal: jest.fn(),
|
||||
isModalOpen: true,
|
||||
closeModal: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<AddToCaseButton timelineId={'timeline-1'} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' });
|
||||
});
|
||||
|
||||
it('navigates to the correct path with id', async () => {
|
||||
useAllCasesModalMock.mockImplementation(({ onRowClick }) => {
|
||||
onRowClick({ id: 'case-id' });
|
||||
|
||||
return {
|
||||
modal: <>{'test'}</>,
|
||||
openModal: jest.fn(),
|
||||
isModalOpen: true,
|
||||
closeModal: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<AddToCaseButton timelineId={'timeline-1'} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/case-id' });
|
||||
});
|
||||
});
|
|
@ -16,9 +16,10 @@ import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions
|
|||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { TimelineStatus, TimelineId, TimelineType } from '../../../../../common/types/timeline';
|
||||
import { getCreateCaseUrl } from '../../../../common/components/link_to';
|
||||
import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
|
||||
import { Case } from '../../../../cases/containers/types';
|
||||
import * as i18n from '../../timeline/properties/translations';
|
||||
|
||||
interface Props {
|
||||
|
@ -42,7 +43,26 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => {
|
|||
)
|
||||
);
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId });
|
||||
|
||||
const onRowClick = useCallback(
|
||||
async (theCase?: Case) => {
|
||||
await navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
|
||||
path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(),
|
||||
});
|
||||
|
||||
dispatch(
|
||||
setInsertTimeline({
|
||||
graphEventId,
|
||||
timelineId,
|
||||
timelineSavedObjectId: savedObjectId,
|
||||
timelineTitle,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, graphEventId, navigateToApp, savedObjectId, timelineId, timelineTitle]
|
||||
);
|
||||
|
||||
const { modal: allCasesModal, openModal: openCaseModal } = useAllCasesModal({ onRowClick });
|
||||
|
||||
const handleButtonClick = useCallback(() => {
|
||||
setPopover((currentIsOpen) => !currentIsOpen);
|
||||
|
@ -79,8 +99,8 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => {
|
|||
|
||||
const handleExistingCaseClick = useCallback(() => {
|
||||
handlePopoverClose();
|
||||
onOpenCaseModal();
|
||||
}, [onOpenCaseModal, handlePopoverClose]);
|
||||
openCaseModal();
|
||||
}, [openCaseModal, handlePopoverClose]);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setPopover(false);
|
||||
|
@ -135,7 +155,7 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => {
|
|||
>
|
||||
<EuiContextMenuPanel items={items} />
|
||||
</EuiPopover>
|
||||
<AllCasesModal />
|
||||
{allCasesModal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -28,9 +28,9 @@ import { isFullScreen } from '../timeline/body/column_headers';
|
|||
import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions';
|
||||
import { Resolver } from '../../../resolver/view';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { useUiSetting$ } from '../../../common/lib/kibana';
|
||||
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const OverlayContainer = styled.div`
|
||||
${({ $restrictWidth }: { $restrictWidth: boolean }) =>
|
||||
|
|
|
@ -51,7 +51,7 @@ describe('EventColumnView', () => {
|
|||
selectedEventIds: {},
|
||||
showCheckboxes: false,
|
||||
showNotes: false,
|
||||
timelineId: 'timeline-1',
|
||||
timelineId: 'timeline-test',
|
||||
toggleShowNotes: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
isEventPinned: false,
|
||||
|
|
|
@ -29,6 +29,7 @@ import { AddEventNoteAction } from '../actions/add_note_icon_item';
|
|||
import { PinEventAction } from '../actions/pin_event_action';
|
||||
import { inputsModel } from '../../../../../common/store';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
@ -138,6 +139,19 @@ export const EventColumnView = React.memo<Props>(
|
|||
/>,
|
||||
]
|
||||
: []),
|
||||
...([
|
||||
TimelineId.detectionsPage,
|
||||
TimelineId.detectionsRulesDetailsPage,
|
||||
TimelineId.active,
|
||||
].includes(timelineId as TimelineId)
|
||||
? [
|
||||
<AddToCaseAction
|
||||
key="attach-to-case"
|
||||
ecsRowData={ecsData}
|
||||
disabled={eventType !== 'signal'}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<AlertContextMenu
|
||||
key="alert-context-menu"
|
||||
ecsRowData={ecsData}
|
||||
|
|
|
@ -38,6 +38,8 @@ interface OwnProps {
|
|||
onRuleChange?: () => void;
|
||||
}
|
||||
|
||||
const NUM_OF_ICON_IN_TIMELINE_ROW = 2;
|
||||
|
||||
export const hasAdditionalActions = (id: TimelineId): boolean =>
|
||||
[TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes(
|
||||
id
|
||||
|
@ -127,7 +129,9 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
getActionsColumnWidth(
|
||||
isEventViewer,
|
||||
showCheckboxes,
|
||||
hasAdditionalActions(id as TimelineId) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0
|
||||
hasAdditionalActions(id as TimelineId)
|
||||
? DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH
|
||||
: 0
|
||||
),
|
||||
[isEventViewer, showCheckboxes, id]
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue