mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Cases] Refactor timeline and cases add alert to new case. Move postComment inside cases (#124831)
This commit is contained in:
parent
4d2bd607eb
commit
05f187aae2
10 changed files with 263 additions and 170 deletions
|
@ -6,11 +6,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import { act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CreateCaseFlyout } from './create_case_flyout';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../../../common/mock';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
|
@ -23,27 +23,21 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
describe('CreateCaseFlyout', () => {
|
||||
let mockedContext: AppMockRenderer;
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppMockRenderer();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders', async () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<CreateCaseFlyout {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId } = mockedContext.render(<CreateCaseFlyout {...defaultProps} />);
|
||||
await act(async () => {
|
||||
expect(getByTestId('create-case-flyout')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('Closing flyout calls onCloseCaseModal', async () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<CreateCaseFlyout {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
it('should call onCloseCaseModal when closing the flyout', async () => {
|
||||
const { getByTestId } = mockedContext.render(<CreateCaseFlyout {...defaultProps} />);
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('euiFlyoutCloseButton'));
|
||||
});
|
||||
|
|
|
@ -11,12 +11,14 @@ import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eu
|
|||
|
||||
import * as i18n from '../translations';
|
||||
import { Case } from '../../../../common/ui/types';
|
||||
import { CreateCaseForm } from '../form';
|
||||
import { CreateCaseForm, CreateCaseAttachment } from '../form';
|
||||
import { UsePostComment } from '../../../containers/use_post_comment';
|
||||
|
||||
export interface CreateCaseFlyoutProps {
|
||||
afterCaseCreated?: (theCase: Case) => Promise<void>;
|
||||
afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise<void>;
|
||||
onClose: () => void;
|
||||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
attachments?: CreateCaseAttachment;
|
||||
}
|
||||
|
||||
const StyledFlyout = styled(EuiFlyout)`
|
||||
|
@ -63,33 +65,36 @@ const FormWrapper = styled.div`
|
|||
`;
|
||||
|
||||
export const CreateCaseFlyout = React.memo<CreateCaseFlyoutProps>(
|
||||
({ afterCaseCreated, onClose, onSuccess }) => (
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<StyledFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="create-case-flyout"
|
||||
// maskProps is needed in order to apply the z-index to the parent overlay element, not to the flyout only
|
||||
maskProps={{ className: maskOverlayClassName }}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{i18n.CREATE_CASE_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<StyledEuiFlyoutBody>
|
||||
<FormWrapper>
|
||||
<CreateCaseForm
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
onCancel={onClose}
|
||||
onSuccess={onSuccess}
|
||||
withSteps={false}
|
||||
/>
|
||||
</FormWrapper>
|
||||
</StyledEuiFlyoutBody>
|
||||
</StyledFlyout>
|
||||
</>
|
||||
)
|
||||
({ afterCaseCreated, onClose, onSuccess, attachments }) => {
|
||||
return (
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<StyledFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="create-case-flyout"
|
||||
// maskProps is needed in order to apply the z-index to the parent overlay element, not to the flyout only
|
||||
maskProps={{ className: maskOverlayClassName }}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{i18n.CREATE_CASE_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<StyledEuiFlyoutBody>
|
||||
<FormWrapper>
|
||||
<CreateCaseForm
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
attachments={attachments}
|
||||
onCancel={onClose}
|
||||
onSuccess={onSuccess}
|
||||
withSteps={false}
|
||||
/>
|
||||
</FormWrapper>
|
||||
</StyledEuiFlyoutBody>
|
||||
</StyledFlyout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CreateCaseFlyout.displayName = 'CreateCaseFlyout';
|
||||
|
|
|
@ -74,7 +74,7 @@ describe('CreateCaseForm', () => {
|
|||
useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
|
||||
});
|
||||
|
||||
it('it renders with steps', async () => {
|
||||
it('renders with steps', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm {...casesFormProps} />
|
||||
|
@ -84,7 +84,7 @@ describe('CreateCaseForm', () => {
|
|||
expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it renders without steps', async () => {
|
||||
it('renders without steps', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm {...casesFormProps} withSteps={false} />
|
||||
|
@ -94,7 +94,7 @@ describe('CreateCaseForm', () => {
|
|||
expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it renders all form fields except case selection', async () => {
|
||||
it('renders all form fields except case selection', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm {...casesFormProps} />
|
||||
|
|
|
@ -23,7 +23,11 @@ import { Tags } from './tags';
|
|||
import { Connector } from './connector';
|
||||
import * as i18n from './translations';
|
||||
import { SyncAlertsToggle } from './sync_alerts_toggle';
|
||||
import { ActionConnector } from '../../../common/api';
|
||||
import {
|
||||
ActionConnector,
|
||||
CommentRequestUserType,
|
||||
CommentRequestAlertType,
|
||||
} from '../../../common/api';
|
||||
import { Case } from '../../containers/types';
|
||||
import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context';
|
||||
import { InsertTimeline } from '../insert_timeline';
|
||||
|
@ -51,6 +55,8 @@ const MySpinner = styled(EuiLoadingSpinner)`
|
|||
left: 50%;
|
||||
z-index: 99;
|
||||
`;
|
||||
export type SupportedCreateCaseAttachment = CommentRequestAlertType | CommentRequestUserType;
|
||||
export type CreateCaseAttachment = SupportedCreateCaseAttachment[];
|
||||
|
||||
export interface CreateCaseFormFieldsProps {
|
||||
connectors: ActionConnector[];
|
||||
|
@ -62,6 +68,7 @@ export interface CreateCaseFormProps extends Pick<Partial<CreateCaseFormFieldsPr
|
|||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise<void>;
|
||||
timelineIntegration?: CasesTimelineIntegration;
|
||||
attachments?: CreateCaseAttachment;
|
||||
}
|
||||
|
||||
const empty: ActionConnector[] = [];
|
||||
|
@ -157,9 +164,20 @@ export const CreateCaseFormFields: React.FC<CreateCaseFormFieldsProps> = React.m
|
|||
CreateCaseFormFields.displayName = 'CreateCaseFormFields';
|
||||
|
||||
export const CreateCaseForm: React.FC<CreateCaseFormProps> = React.memo(
|
||||
({ withSteps = true, afterCaseCreated, onCancel, onSuccess, timelineIntegration }) => (
|
||||
({
|
||||
withSteps = true,
|
||||
afterCaseCreated,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
timelineIntegration,
|
||||
attachments,
|
||||
}) => (
|
||||
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
|
||||
<FormContext afterCaseCreated={afterCaseCreated} onSuccess={onSuccess}>
|
||||
<FormContext
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
onSuccess={onSuccess}
|
||||
attachments={attachments}
|
||||
>
|
||||
<CreateCaseFormFields
|
||||
connectors={empty}
|
||||
isLoadingConnectors={false}
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import { act, RenderResult, waitFor, within } from '@testing-library/react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { ConnectorTypes } from '../../../common/api';
|
||||
import { CommentType, ConnectorTypes } from '../../../common/api';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock';
|
||||
import { usePostCase } from '../../containers/use_post_case';
|
||||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
|
@ -40,6 +40,7 @@ import { CreateCaseFormFields, CreateCaseFormFieldsProps } from './form';
|
|||
import { SubmitCaseButton } from './submit_button';
|
||||
import { usePostPushToService } from '../../containers/use_post_push_to_service';
|
||||
import { Choice } from '../connectors/servicenow/types';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const sampleId = 'case-id';
|
||||
|
||||
|
@ -110,12 +111,30 @@ const fillForm = (wrapper: ReactWrapper) => {
|
|||
});
|
||||
};
|
||||
|
||||
const fillFormReactTestingLib = async (renderResult: RenderResult) => {
|
||||
const titleInput = within(renderResult.getByTestId('caseTitle')).getByTestId('input');
|
||||
userEvent.type(titleInput, sampleData.title);
|
||||
|
||||
const descriptionInput = renderResult.container.querySelector(
|
||||
`[data-test-subj="caseDescription"] textarea`
|
||||
);
|
||||
if (descriptionInput) {
|
||||
userEvent.type(descriptionInput, sampleData.description);
|
||||
}
|
||||
const caseTags = renderResult.getByTestId('caseTags');
|
||||
for (let i = 0; i < sampleTags.length; i++) {
|
||||
const tagsInput = await within(caseTags).findByTestId('comboBoxInput');
|
||||
userEvent.type(tagsInput, `${sampleTags[i]}{enter}`);
|
||||
}
|
||||
};
|
||||
|
||||
describe('Create case', () => {
|
||||
const fetchTags = jest.fn();
|
||||
const onFormSubmitSuccess = jest.fn();
|
||||
const afterCaseCreated = jest.fn();
|
||||
const postComment = jest.fn();
|
||||
let onChoicesSuccess: (values: Choice[]) => void;
|
||||
let mockedContext: AppMockRenderer;
|
||||
|
||||
beforeAll(() => {
|
||||
postCase.mockResolvedValue({
|
||||
|
@ -149,29 +168,23 @@ describe('Create case', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppMockRenderer();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Step 1 - Case Fields', () => {
|
||||
it('it renders', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
it('renders correctly', async () => {
|
||||
const renderResult = mockedContext.render(
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.update();
|
||||
});
|
||||
expect(wrapper.find(`[data-test-subj="caseTitle"]`).first().exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseDescription"]`).first().exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseTags"]`).first().exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).first().exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-creation-form-steps"]`).first().exists()
|
||||
).toBeTruthy();
|
||||
expect(renderResult.getByTestId('caseTitle')).toBeTruthy();
|
||||
expect(renderResult.getByTestId('caseDescription')).toBeTruthy();
|
||||
expect(renderResult.getByTestId('caseTags')).toBeTruthy();
|
||||
expect(renderResult.getByTestId('caseConnectors')).toBeTruthy();
|
||||
expect(renderResult.getByTestId('case-creation-form-steps')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should post case on submit click', async () => {
|
||||
|
@ -180,21 +193,21 @@ describe('Create case', () => {
|
|||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
const renderResult = mockedContext.render(
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
);
|
||||
|
||||
fillForm(wrapper);
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
await waitFor(() => expect(postCase).toBeCalledWith(sampleData));
|
||||
await fillFormReactTestingLib(renderResult);
|
||||
userEvent.click(renderResult.getByTestId('create-case-submit'));
|
||||
await waitFor(() => {
|
||||
expect(postCase).toBeCalledWith(sampleData);
|
||||
});
|
||||
});
|
||||
|
||||
it('it does not submits the title when the length is longer than 64 characters', async () => {
|
||||
it('does not submits the title when the length is longer than 64 characters', async () => {
|
||||
const longTitle =
|
||||
'This is a title that should not be saved as it is longer than 64 characters.';
|
||||
|
||||
|
@ -271,7 +284,7 @@ describe('Create case', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('it should select the default connector set in the configuration', async () => {
|
||||
it('should select the default connector set in the configuration', async () => {
|
||||
useCaseConfigureMock.mockImplementation(() => ({
|
||||
...useCaseConfigureResponse,
|
||||
connector: {
|
||||
|
@ -321,7 +334,7 @@ describe('Create case', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('it should default to none if the default connector does not exist in connectors', async () => {
|
||||
it('should default to none if the default connector does not exist in connectors', async () => {
|
||||
useCaseConfigureMock.mockImplementation(() => ({
|
||||
...useCaseConfigureResponse,
|
||||
connector: {
|
||||
|
@ -357,7 +370,7 @@ describe('Create case', () => {
|
|||
});
|
||||
|
||||
describe('Step 2 - Connector Fields', () => {
|
||||
it(`it should submit and push to Jira connector`, async () => {
|
||||
it(`should submit and push to Jira connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
|
@ -424,7 +437,7 @@ describe('Create case', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`it should submit and push to resilient connector`, async () => {
|
||||
it(`should submit and push to resilient connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
|
@ -494,7 +507,7 @@ describe('Create case', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`it should submit and push to servicenow itsm connector`, async () => {
|
||||
it(`should submit and push to servicenow itsm connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
|
@ -589,7 +602,7 @@ describe('Create case', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`it should submit and push to servicenow sir connector`, async () => {
|
||||
it(`should submit and push to servicenow sir connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
|
@ -692,32 +705,28 @@ describe('Create case', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`it should call afterCaseCreated`, async () => {
|
||||
it(`should call afterCaseCreated`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}>
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
const wrapper = mockedContext.render(
|
||||
<FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}>
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
);
|
||||
|
||||
fillForm(wrapper);
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy();
|
||||
await fillFormReactTestingLib(wrapper);
|
||||
expect(wrapper.queryByTestId('connector-fields-jira')).toBeFalsy();
|
||||
userEvent.click(wrapper.getByTestId('dropdown-connectors'));
|
||||
await act(async () => {
|
||||
userEvent.click(wrapper.getByTestId('dropdown-connector-jira-1'));
|
||||
});
|
||||
expect(wrapper.getByTestId('connector-fields-jira')).toBeTruthy();
|
||||
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
userEvent.click(wrapper.getByTestId('create-case-submit'));
|
||||
await waitFor(() => {
|
||||
expect(afterCaseCreated).toHaveBeenCalledWith(
|
||||
{
|
||||
|
@ -729,15 +738,75 @@ describe('Create case', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`it should call callbacks in correct order`, async () => {
|
||||
it('should call `postComment` with the attachments after the case is created', async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
const attachments = [
|
||||
{
|
||||
alertId: '1234',
|
||||
index: '',
|
||||
rule: {
|
||||
id: '45321',
|
||||
name: 'my rule',
|
||||
},
|
||||
owner: 'owner',
|
||||
type: CommentType.alert as const,
|
||||
},
|
||||
{
|
||||
alertId: '7896',
|
||||
index: '',
|
||||
rule: {
|
||||
id: '445324',
|
||||
name: 'my rule',
|
||||
},
|
||||
owner: 'second-owner',
|
||||
type: CommentType.alert as const,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = mockedContext.render(
|
||||
<FormContext onSuccess={onFormSubmitSuccess} attachments={attachments}>
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
);
|
||||
|
||||
await fillFormReactTestingLib(wrapper);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(wrapper.getByTestId('create-case-submit'));
|
||||
});
|
||||
expect(postComment).toHaveBeenCalledWith({ caseId: 'case-id', data: attachments[0] });
|
||||
expect(postComment).toHaveBeenCalledWith({ caseId: 'case-id', data: attachments[1] });
|
||||
});
|
||||
|
||||
it(`should call callbacks in correct order`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
const attachments = [
|
||||
{
|
||||
alertId: '1234',
|
||||
index: '',
|
||||
rule: {
|
||||
id: '45321',
|
||||
name: 'my rule',
|
||||
},
|
||||
owner: 'owner',
|
||||
type: CommentType.alert as const,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}>
|
||||
<FormContext
|
||||
onSuccess={onFormSubmitSuccess}
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
attachments={attachments}
|
||||
>
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
|
@ -757,26 +826,23 @@ describe('Create case', () => {
|
|||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(postCase).toHaveBeenCalled();
|
||||
expect(postComment).toHaveBeenCalled();
|
||||
expect(afterCaseCreated).toHaveBeenCalled();
|
||||
expect(pushCaseToExternalService).toHaveBeenCalled();
|
||||
expect(onFormSubmitSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const postCaseOrder = postCase.mock.invocationCallOrder[0];
|
||||
const postCommentOrder = postComment.mock.invocationCallOrder[0];
|
||||
const afterCaseOrder = afterCaseCreated.mock.invocationCallOrder[0];
|
||||
const pushCaseToExternalServiceOrder = pushCaseToExternalService.mock.invocationCallOrder[0];
|
||||
const onFormSubmitSuccessOrder = onFormSubmitSuccess.mock.invocationCallOrder[0];
|
||||
|
||||
expect(
|
||||
postCaseOrder < afterCaseOrder &&
|
||||
postCaseOrder < pushCaseToExternalServiceOrder &&
|
||||
postCaseOrder < onFormSubmitSuccessOrder
|
||||
postCaseOrder < postCommentOrder &&
|
||||
postCommentOrder < afterCaseOrder &&
|
||||
afterCaseOrder < pushCaseToExternalServiceOrder &&
|
||||
pushCaseToExternalServiceOrder < onFormSubmitSuccessOrder
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
afterCaseOrder < pushCaseToExternalServiceOrder && afterCaseOrder < onFormSubmitSuccessOrder
|
||||
).toBe(true);
|
||||
|
||||
expect(pushCaseToExternalServiceOrder < onFormSubmitSuccessOrder).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ import { UsePostComment, usePostComment } from '../../containers/use_post_commen
|
|||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { useCasesFeatures } from '../cases_context/use_cases_features';
|
||||
import { getConnectorById } from '../utils';
|
||||
import { CreateCaseAttachment } from './form';
|
||||
|
||||
const initialCaseValue: FormProps = {
|
||||
description: '',
|
||||
|
@ -34,9 +35,15 @@ interface Props {
|
|||
afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise<void>;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
onSuccess?: (theCase: Case) => Promise<void>;
|
||||
attachments?: CreateCaseAttachment;
|
||||
}
|
||||
|
||||
export const FormContext: React.FC<Props> = ({ afterCaseCreated, children, onSuccess }) => {
|
||||
export const FormContext: React.FC<Props> = ({
|
||||
afterCaseCreated,
|
||||
children,
|
||||
onSuccess,
|
||||
attachments,
|
||||
}) => {
|
||||
const { connectors, loading: isLoadingConnectors } = useConnectors();
|
||||
const { owner } = useCasesContext();
|
||||
const { isSyncAlertsEnabled } = useCasesFeatures();
|
||||
|
@ -69,6 +76,19 @@ export const FormContext: React.FC<Props> = ({ afterCaseCreated, children, onSuc
|
|||
owner: selectedOwner ?? owner[0],
|
||||
});
|
||||
|
||||
// add attachments to the case
|
||||
if (updatedCase && Array.isArray(attachments)) {
|
||||
// TODO currently the API only supports to add a comment at the time
|
||||
// once the API is updated we should use bulk post comment #124814
|
||||
// this operation is intentionally made in sequence
|
||||
for (const attachment of attachments) {
|
||||
await postComment({
|
||||
caseId: updatedCase.id,
|
||||
data: attachment,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (afterCaseCreated && updatedCase) {
|
||||
await afterCaseCreated(updatedCase, postComment);
|
||||
}
|
||||
|
@ -92,6 +112,7 @@ export const FormContext: React.FC<Props> = ({ afterCaseCreated, children, onSuc
|
|||
owner,
|
||||
afterCaseCreated,
|
||||
onSuccess,
|
||||
attachments,
|
||||
postComment,
|
||||
pushCaseToExternalService,
|
||||
]
|
||||
|
|
|
@ -22,6 +22,7 @@ export const getCreateCaseFlyoutLazy = ({
|
|||
afterCaseCreated,
|
||||
onClose,
|
||||
onSuccess,
|
||||
attachments,
|
||||
}: GetCreateCaseFlyoutProps) => (
|
||||
<CasesProvider value={{ owner, userCanCrud, features }}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
|
@ -29,6 +30,7 @@ export const getCreateCaseFlyoutLazy = ({
|
|||
afterCaseCreated={afterCaseCreated}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
attachments={attachments}
|
||||
/>
|
||||
</Suspense>
|
||||
</CasesProvider>
|
||||
|
|
|
@ -7,7 +7,16 @@
|
|||
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { CaseStatuses, StatusAll, CasesFeatures } from '../../../../../../cases/common';
|
||||
import {
|
||||
GetAllCasesSelectorModalProps,
|
||||
GetCreateCaseFlyoutProps,
|
||||
} from '../../../../../../cases/public';
|
||||
import {
|
||||
CaseStatuses,
|
||||
StatusAll,
|
||||
CasesFeatures,
|
||||
CommentType,
|
||||
} from '../../../../../../cases/common';
|
||||
import { TimelineItem } from '../../../../../common/search_strategy';
|
||||
import { useAddToCase, normalizedEventFields } from '../../../../hooks/use_add_to_case';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
|
@ -43,12 +52,12 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
const {
|
||||
onCaseClicked,
|
||||
onCaseSuccess,
|
||||
attachAlertToCase,
|
||||
onCaseCreated,
|
||||
isAllCaseModalOpen,
|
||||
isCreateCaseFlyoutOpen,
|
||||
} = useAddToCase({ event, casePermissions, appId, owner, onClose });
|
||||
|
||||
const allCasesSelectorModalProps = useMemo(() => {
|
||||
const allCasesSelectorModalProps: GetAllCasesSelectorModalProps = useMemo(() => {
|
||||
const { ruleId, ruleName } = normalizedEventFields(event);
|
||||
return {
|
||||
alertData: {
|
||||
|
@ -86,23 +95,40 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
dispatch(setOpenAddToNewCase({ id: eventId, isOpen: false }));
|
||||
}, [dispatch, eventId]);
|
||||
|
||||
const createCaseFlyoutProps = useMemo(() => {
|
||||
const createCaseFlyoutProps: GetCreateCaseFlyoutProps = useMemo(() => {
|
||||
const { ruleId, ruleName } = normalizedEventFields(event);
|
||||
const attachments = [
|
||||
{
|
||||
alertId: eventId,
|
||||
index: eventIndex ?? '',
|
||||
rule: {
|
||||
id: ruleId,
|
||||
name: ruleName,
|
||||
},
|
||||
owner,
|
||||
type: CommentType.alert as const,
|
||||
},
|
||||
];
|
||||
return {
|
||||
afterCaseCreated: attachAlertToCase,
|
||||
afterCaseCreated: onCaseCreated,
|
||||
onClose: closeCaseFlyoutOpen,
|
||||
onSuccess: onCaseSuccess,
|
||||
useInsertTimeline,
|
||||
owner: [owner],
|
||||
userCanCrud: casePermissions?.crud ?? false,
|
||||
features: casesFeatures,
|
||||
attachments,
|
||||
};
|
||||
}, [
|
||||
attachAlertToCase,
|
||||
event,
|
||||
eventId,
|
||||
eventIndex,
|
||||
owner,
|
||||
onCaseCreated,
|
||||
closeCaseFlyoutOpen,
|
||||
onCaseSuccess,
|
||||
useInsertTimeline,
|
||||
owner,
|
||||
casePermissions,
|
||||
casePermissions?.crud,
|
||||
casesFeatures,
|
||||
]);
|
||||
|
||||
|
@ -113,6 +139,7 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
</>
|
||||
);
|
||||
};
|
||||
AddToCaseActionComponent.displayName = 'AddToCaseAction';
|
||||
|
||||
export const AddToCaseAction = memo(AddToCaseActionComponent);
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ const AddToNewCaseButtonComponent: React.FC<AddToNewCaseButtonProps> = ({
|
|||
</>
|
||||
);
|
||||
};
|
||||
AddToNewCaseButtonComponent.displayName = 'AddToNewCaseButton';
|
||||
|
||||
export const AddToNewCaseButton = memo(AddToNewCaseButtonComponent);
|
||||
|
||||
|
|
|
@ -23,11 +23,7 @@ interface UseAddToCase {
|
|||
addExistingCaseClick: () => void;
|
||||
onCaseClicked: (theCase?: Case) => void;
|
||||
onCaseSuccess: (theCase: Case) => Promise<void>;
|
||||
attachAlertToCase: (
|
||||
theCase: Case,
|
||||
postComment?: ((arg: PostCommentArg) => Promise<void>) | undefined,
|
||||
updateCase?: ((newCase: Case) => void) | undefined
|
||||
) => Promise<void>;
|
||||
onCaseCreated: () => Promise<void>;
|
||||
isAllCaseModalOpen: boolean;
|
||||
isDisabled: boolean;
|
||||
userCanCrud: boolean;
|
||||
|
@ -38,27 +34,13 @@ interface UseAddToCase {
|
|||
isCreateCaseFlyoutOpen: boolean;
|
||||
}
|
||||
|
||||
interface PostCommentArg {
|
||||
caseId: string;
|
||||
data: {
|
||||
type: 'alert';
|
||||
alertId: string | string[];
|
||||
index: string | string[];
|
||||
rule: { id: string | null; name: string | null };
|
||||
owner: string;
|
||||
};
|
||||
updateCase?: (newCase: Case) => void;
|
||||
}
|
||||
|
||||
export const useAddToCase = ({
|
||||
event,
|
||||
casePermissions,
|
||||
appId,
|
||||
owner,
|
||||
onClose,
|
||||
}: AddToCaseActionProps): UseAddToCase => {
|
||||
const eventId = event?.ecs._id ?? '';
|
||||
const eventIndex = event?.ecs._index ?? '';
|
||||
const dispatch = useDispatch();
|
||||
// TODO: use correct value in standalone or integrated.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -116,33 +98,10 @@ export const useAddToCase = ({
|
|||
[navigateToApp, appId]
|
||||
);
|
||||
|
||||
const attachAlertToCase = useCallback(
|
||||
async (
|
||||
theCase: Case,
|
||||
postComment?: (arg: PostCommentArg) => Promise<void>,
|
||||
updateCase?: (newCase: Case) => void
|
||||
) => {
|
||||
dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false }));
|
||||
const { ruleId, ruleName } = normalizedEventFields(event);
|
||||
if (postComment) {
|
||||
await postComment({
|
||||
caseId: theCase.id,
|
||||
data: {
|
||||
type: 'alert',
|
||||
alertId: eventId,
|
||||
index: eventIndex ?? '',
|
||||
rule: {
|
||||
id: ruleId,
|
||||
name: ruleName,
|
||||
},
|
||||
owner,
|
||||
},
|
||||
updateCase,
|
||||
});
|
||||
}
|
||||
},
|
||||
[eventId, eventIndex, owner, dispatch, event]
|
||||
);
|
||||
const onCaseCreated = useCallback(async () => {
|
||||
dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false }));
|
||||
}, [eventId, dispatch]);
|
||||
|
||||
const onCaseSuccess = useCallback(
|
||||
async (theCase: Case) => {
|
||||
dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false }));
|
||||
|
@ -185,7 +144,7 @@ export const useAddToCase = ({
|
|||
addExistingCaseClick,
|
||||
onCaseClicked,
|
||||
onCaseSuccess,
|
||||
attachAlertToCase,
|
||||
onCaseCreated,
|
||||
isAllCaseModalOpen,
|
||||
isDisabled,
|
||||
userCanCrud,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue