mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solutino][Case] Case connector alert UI (#82405)
Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
49f0ca0827
commit
b9a64ba7d5
49 changed files with 2036 additions and 626 deletions
|
@ -160,6 +160,7 @@ export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID];
|
|||
/*
|
||||
Rule notifications options
|
||||
*/
|
||||
export const ENABLE_CASE_CONNECTOR = false;
|
||||
export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [
|
||||
'.email',
|
||||
'.slack',
|
||||
|
@ -169,6 +170,11 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [
|
|||
'.jira',
|
||||
'.resilient',
|
||||
];
|
||||
|
||||
if (ENABLE_CASE_CONNECTOR) {
|
||||
NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.push('.case');
|
||||
}
|
||||
|
||||
export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions';
|
||||
export const NOTIFICATION_THROTTLE_RULE = 'rule';
|
||||
|
||||
|
|
|
@ -6,42 +6,24 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { waitFor, act } from '@testing-library/react';
|
||||
|
||||
import { AddComment, AddCommentRefObject } from '.';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { getFormMock } from '../__mock__/form';
|
||||
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
|
||||
|
||||
import { CommentRequest, CommentType } from '../../../../../case/common/api';
|
||||
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
|
||||
import { useInsertTimeline } from '../use_insert_timeline';
|
||||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form';
|
||||
import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data';
|
||||
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
jest.mock(
|
||||
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'
|
||||
);
|
||||
|
||||
jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline');
|
||||
jest.mock('../../containers/use_post_comment');
|
||||
jest.mock('../use_insert_timeline');
|
||||
|
||||
const useFormMock = useForm as jest.Mock;
|
||||
const useFormDataMock = useFormData as jest.Mock;
|
||||
|
||||
const useInsertTimelineMock = useInsertTimeline as jest.Mock;
|
||||
const usePostCommentMock = usePostComment as jest.Mock;
|
||||
|
||||
const useInsertTimelineMock = useInsertTimeline as jest.Mock;
|
||||
const onCommentSaving = jest.fn();
|
||||
const onCommentPosted = jest.fn();
|
||||
const postComment = jest.fn();
|
||||
const handleCursorChange = jest.fn();
|
||||
const handleOnTimelineChange = jest.fn();
|
||||
|
||||
const addCommentProps = {
|
||||
caseId: '1234',
|
||||
|
@ -52,15 +34,6 @@ const addCommentProps = {
|
|||
showLoading: false,
|
||||
};
|
||||
|
||||
const defaultInsertTimeline = {
|
||||
cursorPosition: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
handleCursorChange,
|
||||
handleOnTimelineChange,
|
||||
};
|
||||
|
||||
const defaultPostCommment = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
@ -73,14 +46,9 @@ const sampleData: CommentRequest = {
|
|||
};
|
||||
|
||||
describe('AddComment ', () => {
|
||||
const formHookMock = getFormMock(sampleData);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline);
|
||||
usePostCommentMock.mockImplementation(() => defaultPostCommment);
|
||||
useFormMock.mockImplementation(() => ({ form: formHookMock }));
|
||||
useFormDataMock.mockImplementation(() => [{ comment: sampleData.comment }]);
|
||||
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
|
||||
});
|
||||
|
||||
|
@ -92,14 +60,25 @@ describe('AddComment ', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find(`[data-test-subj="add-comment"] textarea`)
|
||||
.first()
|
||||
.simulate('change', { target: { value: sampleData.comment } });
|
||||
});
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy();
|
||||
|
||||
wrapper.find(`[data-test-subj="submit-comment"]`).first().simulate('click');
|
||||
await act(async () => {
|
||||
wrapper.find(`[data-test-subj="submit-comment"]`).first().simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onCommentSaving).toBeCalled();
|
||||
expect(postComment).toBeCalledWith(sampleData, onCommentPosted);
|
||||
expect(formHookMock.reset).toBeCalled();
|
||||
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -112,6 +91,7 @@ describe('AddComment ', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="submit-comment"]`).first().prop('isDisabled')
|
||||
|
@ -127,15 +107,16 @@ describe('AddComment ', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="submit-comment"]`).first().prop('isDisabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should insert a quote', () => {
|
||||
it('should insert a quote', async () => {
|
||||
const sampleQuote = 'what a cool quote';
|
||||
const ref = React.createRef<AddCommentRefObject>();
|
||||
mount(
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<AddComment {...{ ...addCommentProps }} ref={ref} />
|
||||
|
@ -143,10 +124,37 @@ describe('AddComment ', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
ref.current!.addQuote(sampleQuote);
|
||||
expect(formHookMock.setFieldValue).toBeCalledWith(
|
||||
'comment',
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find(`[data-test-subj="add-comment"] textarea`)
|
||||
.first()
|
||||
.simulate('change', { target: { value: sampleData.comment } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
ref.current!.addQuote(sampleQuote);
|
||||
});
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(
|
||||
`${sampleData.comment}\n\n${sampleQuote}`
|
||||
);
|
||||
});
|
||||
|
||||
it('it should insert a timeline', async () => {
|
||||
useInsertTimelineMock.mockImplementation((comment, onTimelineAttached) => {
|
||||
onTimelineAttached(`[title](url)`);
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<AddComment {...{ ...addCommentProps }} />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,12 +12,11 @@ import { CommentType } from '../../../../../case/common/api';
|
|||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { Case } from '../../containers/types';
|
||||
import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form';
|
||||
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
|
||||
import { Form, useForm, UseField, useFormData } from '../../../shared_imports';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { schema, AddCommentFormSchema } from './schema';
|
||||
import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click';
|
||||
import { useInsertTimeline } from '../use_insert_timeline';
|
||||
|
||||
const MySpinner = styled(EuiLoadingSpinner)`
|
||||
position: absolute;
|
||||
|
@ -56,12 +55,6 @@ export const AddComment = React.memo(
|
|||
const { setFieldValue, reset, submit } = form;
|
||||
const [{ comment }] = useFormData<{ comment: string }>({ form, watch: [fieldName] });
|
||||
|
||||
const onCommentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
const { handleCursorChange } = useInsertTimeline(comment, onCommentChange);
|
||||
|
||||
const addQuote = useCallback(
|
||||
(quote) => {
|
||||
setFieldValue(fieldName, `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`);
|
||||
|
@ -73,7 +66,12 @@ export const AddComment = React.memo(
|
|||
addQuote,
|
||||
}));
|
||||
|
||||
const handleTimelineClick = useTimelineClick();
|
||||
const onTimelineAttached = useCallback(
|
||||
(newValue: string) => setFieldValue(fieldName, newValue),
|
||||
[setFieldValue]
|
||||
);
|
||||
|
||||
useInsertTimeline(comment ?? '', onTimelineAttached);
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
const { isValid, data } = await submit();
|
||||
|
@ -98,8 +96,6 @@ export const AddComment = React.memo(
|
|||
isDisabled: isLoading,
|
||||
dataTestSubj: 'add-comment',
|
||||
placeholder: i18n.ADD_COMMENT_HELP_TEXT,
|
||||
onCursorPositionUpdate: handleCursorChange,
|
||||
onClickTimeline: handleTimelineClick,
|
||||
bottomRightContent: (
|
||||
<EuiButton
|
||||
data-test-subj="submit-comment"
|
||||
|
|
|
@ -13,7 +13,6 @@ import { TestProviders } from '../../../common/mock';
|
|||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { useGetReporters } from '../../containers/use_get_reporters';
|
||||
import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases';
|
||||
jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline');
|
||||
jest.mock('../../containers/use_get_reporters');
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { ActionConnector } from '../../containers/configure/types';
|
||||
import { connectorsConfiguration } from '../../../common/lib/connectors/config';
|
||||
import { connectorsConfiguration } from '../connectors';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface Props {
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
|
||||
import { connectorsConfiguration } from '../../../common/lib/connectors/config';
|
||||
import { createDefaultMapping } from '../../../common/lib/connectors/utils';
|
||||
import { connectorsConfiguration, createDefaultMapping } from '../connectors';
|
||||
|
||||
import { FieldMapping, FieldMappingProps } from './field_mapping';
|
||||
import { mapping } from './__mock__';
|
||||
|
|
|
@ -14,16 +14,16 @@ import {
|
|||
ActionType,
|
||||
ThirdPartyField,
|
||||
} from '../../containers/configure/types';
|
||||
import { FieldMappingRow } from './field_mapping_row';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { connectorsConfiguration } from '../../../common/lib/connectors/config';
|
||||
import { setActionTypeToMapping, setThirdPartyToMapping } from './utils';
|
||||
import {
|
||||
ThirdPartyField as ConnectorConfigurationThirdPartyField,
|
||||
AllThirdPartyFields,
|
||||
} from '../../../common/lib/connectors/types';
|
||||
import { createDefaultMapping } from '../../../common/lib/connectors/utils';
|
||||
createDefaultMapping,
|
||||
connectorsConfiguration,
|
||||
} from '../connectors';
|
||||
|
||||
import { FieldMappingRow } from './field_mapping_row';
|
||||
import * as i18n from './translations';
|
||||
import { setActionTypeToMapping, setThirdPartyToMapping } from './utils';
|
||||
|
||||
const FieldRowWrapper = styled.div`
|
||||
margin-top: 8px;
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
|
||||
import { capitalize } from 'lodash/fp';
|
||||
import { CaseField, ActionType, ThirdPartyField } from '../../containers/configure/types';
|
||||
import { AllThirdPartyFields } from '../../../common/lib/connectors/types';
|
||||
import { AllThirdPartyFields } from '../connectors';
|
||||
|
||||
export interface RowProps {
|
||||
id: string;
|
||||
|
|
|
@ -18,7 +18,7 @@ import { ClosureType } from '../../containers/configure/types';
|
|||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionConnectorTableItem } from '../../../../../triggers_actions_ui/public/types';
|
||||
import { connectorsConfiguration } from '../../../common/lib/connectors/config';
|
||||
import { connectorsConfiguration } from '../connectors';
|
||||
|
||||
import { SectionWrapper } from '../wrappers';
|
||||
import { Connectors } from './connectors';
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { UseField, Form, useForm, FormHook } from '../../../shared_imports';
|
||||
import { ConnectorSelector } from './form';
|
||||
import { connectorsMock } from '../../containers/mock';
|
||||
import { getFormMock } from '../__mock__/form';
|
||||
|
||||
jest.mock(
|
||||
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
|
||||
);
|
||||
|
||||
const useFormMock = useForm as jest.Mock;
|
||||
|
||||
describe('ConnectorSelector', () => {
|
||||
const formHookMock = getFormMock({ connectorId: connectorsMock[0].id });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useFormMock.mockImplementation(() => ({ form: formHookMock }));
|
||||
});
|
||||
|
||||
it('it should render', async () => {
|
||||
const wrapper = mount(
|
||||
<Form form={(formHookMock as unknown) as FormHook}>
|
||||
<UseField
|
||||
path="connectorId"
|
||||
component={ConnectorSelector}
|
||||
componentProps={{
|
||||
connectors: connectorsMock,
|
||||
dataTestSubj: 'caseConnectors',
|
||||
disabled: false,
|
||||
idAria: 'caseConnectors',
|
||||
isLoading: false,
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it should not render when is not in edit mode', async () => {
|
||||
const wrapper = mount(
|
||||
<Form form={(formHookMock as unknown) as FormHook}>
|
||||
<UseField
|
||||
path="connectorId"
|
||||
component={ConnectorSelector}
|
||||
componentProps={{
|
||||
connectors: connectorsMock,
|
||||
dataTestSubj: 'caseConnectors',
|
||||
disabled: false,
|
||||
idAria: 'caseConnectors',
|
||||
isLoading: false,
|
||||
isEdit: false,
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -4,8 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
|
||||
import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown';
|
||||
|
@ -14,9 +15,8 @@ import { ActionConnector } from '../../../../../case/common/api/cases';
|
|||
interface ConnectorSelectorProps {
|
||||
connectors: ActionConnector[];
|
||||
dataTestSubj: string;
|
||||
defaultValue?: ActionConnector;
|
||||
disabled: boolean;
|
||||
field: FieldHook;
|
||||
field: FieldHook<string>;
|
||||
idAria: string;
|
||||
isEdit: boolean;
|
||||
isLoading: boolean;
|
||||
|
@ -24,7 +24,6 @@ interface ConnectorSelectorProps {
|
|||
export const ConnectorSelector = ({
|
||||
connectors,
|
||||
dataTestSubj,
|
||||
defaultValue,
|
||||
disabled = false,
|
||||
field,
|
||||
idAria,
|
||||
|
@ -32,19 +31,6 @@ export const ConnectorSelector = ({
|
|||
isLoading = false,
|
||||
}: ConnectorSelectorProps) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
useEffect(() => {
|
||||
field.setValue(defaultValue);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValue]);
|
||||
|
||||
const handleContentChange = useCallback(
|
||||
(newConnector: string) => {
|
||||
field.setValue(newConnector);
|
||||
},
|
||||
[field]
|
||||
);
|
||||
|
||||
return isEdit ? (
|
||||
<EuiFormRow
|
||||
data-test-subj={dataTestSubj}
|
||||
|
@ -60,8 +46,8 @@ export const ConnectorSelector = ({
|
|||
connectors={connectors}
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
onChange={handleContentChange}
|
||||
selectedConnector={(field.value as string) ?? 'none'}
|
||||
onChange={field.setValue}
|
||||
selectedConnector={isEmpty(field.value) ? 'none' : field.value}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null;
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui';
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import { Case } from '../../../containers/types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface CaseDropdownProps {
|
||||
isLoading: boolean;
|
||||
cases: Case[];
|
||||
selectedCase?: string;
|
||||
onCaseChanged: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ADD_CASE_BUTTON_ID = 'add-case';
|
||||
|
||||
const addNewCase = {
|
||||
value: ADD_CASE_BUTTON_ID,
|
||||
inputDisplay: (
|
||||
<span className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushLeft">
|
||||
{i18n.CASE_CONNECTOR_ADD_NEW_CASE}
|
||||
</span>
|
||||
),
|
||||
'data-test-subj': 'dropdown-connector-add-connector',
|
||||
};
|
||||
|
||||
const CasesDropdownComponent: React.FC<CaseDropdownProps> = ({
|
||||
isLoading,
|
||||
cases,
|
||||
selectedCase,
|
||||
onCaseChanged,
|
||||
}) => {
|
||||
const caseOptions: Array<EuiSuperSelectOption<string>> = useMemo(
|
||||
() =>
|
||||
cases.reduce<Array<EuiSuperSelectOption<string>>>(
|
||||
(acc, theCase) => [
|
||||
...acc,
|
||||
{
|
||||
value: theCase.id,
|
||||
inputDisplay: <span>{theCase.title}</span>,
|
||||
'data-test-subj': `case-connector-cases-dropdown-${theCase.id}`,
|
||||
},
|
||||
],
|
||||
[]
|
||||
),
|
||||
[cases]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...caseOptions, addNewCase], [caseOptions]);
|
||||
const onChange = useCallback((id: string) => onCaseChanged(id), [onCaseChanged]);
|
||||
|
||||
return (
|
||||
<EuiFormRow label={i18n.CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL} fullWidth={true}>
|
||||
<EuiSuperSelect
|
||||
options={options}
|
||||
data-test-subj="case-connector-cases-dropdown"
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
isLoading={isLoading}
|
||||
valueOfSelected={selectedCase}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const CasesDropdown = memo(CasesDropdownComponent);
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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, useMemo, useCallback } from 'react';
|
||||
import { useGetCases } from '../../../containers/use_get_cases';
|
||||
import { useCreateCaseModal } from '../../use_create_case_modal';
|
||||
import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown';
|
||||
|
||||
interface ExistingCaseProps {
|
||||
selectedCase: string | null;
|
||||
onCaseChanged: (id: string) => void;
|
||||
}
|
||||
|
||||
const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, selectedCase }) => {
|
||||
const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases();
|
||||
|
||||
const onCaseCreated = useCallback(() => refetchCases(), [refetchCases]);
|
||||
|
||||
const { Modal: CreateCaseModal, openModal } = useCreateCaseModal({ onCaseCreated });
|
||||
|
||||
const onChange = useCallback(
|
||||
(id: string) => {
|
||||
if (id === ADD_CASE_BUTTON_ID) {
|
||||
openModal();
|
||||
return;
|
||||
}
|
||||
|
||||
onCaseChanged(id);
|
||||
},
|
||||
[onCaseChanged, openModal]
|
||||
);
|
||||
|
||||
const isCasesLoading = useMemo(
|
||||
() => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'),
|
||||
[isLoadingCases]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CasesDropdown
|
||||
isLoading={isCasesLoading}
|
||||
cases={cases.cases}
|
||||
selectedCase={selectedCase ?? undefined}
|
||||
onCaseChanged={onChange}
|
||||
/>
|
||||
<CreateCaseModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExistingCase = memo(ExistingCaseComponent);
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import { ActionParamsProps } from '../../../../../../triggers_actions_ui/public/types';
|
||||
import { CommentType } from '../../../../../../case/common/api';
|
||||
|
||||
import { CaseActionParams } from './types';
|
||||
import { ExistingCase } from './existing_case';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const Container = styled.div`
|
||||
${({ theme }) => `
|
||||
margin-top: ${theme.eui?.euiSize ?? '16px'};
|
||||
`}
|
||||
`;
|
||||
|
||||
const defaultAlertComment = {
|
||||
type: CommentType.alert,
|
||||
alertId: '{{context.rule.id}}',
|
||||
index: '{{context.rule.output_index}}',
|
||||
};
|
||||
|
||||
const CaseParamsFields: React.FunctionComponent<ActionParamsProps<CaseActionParams>> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
actionConnector,
|
||||
}) => {
|
||||
const { caseId = null, comment = defaultAlertComment } = actionParams.subActionParams ?? {};
|
||||
|
||||
const [selectedCase, setSelectedCase] = useState<string | null>(null);
|
||||
|
||||
const editSubActionProperty = useCallback(
|
||||
(key: string, value: unknown) => {
|
||||
const newProps = { ...actionParams.subActionParams, [key]: value };
|
||||
editAction('subActionParams', newProps, index);
|
||||
},
|
||||
[actionParams.subActionParams, editAction, index]
|
||||
);
|
||||
|
||||
const onCaseChanged = useCallback(
|
||||
(id: string) => {
|
||||
setSelectedCase(id);
|
||||
editSubActionProperty('caseId', id);
|
||||
},
|
||||
[editSubActionProperty]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionParams.subAction) {
|
||||
editAction('subAction', 'addComment', index);
|
||||
}
|
||||
|
||||
if (!actionParams.subActionParams?.caseId) {
|
||||
editSubActionProperty('caseId', caseId);
|
||||
}
|
||||
|
||||
if (!actionParams.subActionParams?.comment) {
|
||||
editSubActionProperty('comment', comment);
|
||||
}
|
||||
|
||||
if (caseId != null) {
|
||||
setSelectedCase((prevCaseId) => (prevCaseId !== caseId ? caseId : prevCaseId));
|
||||
}
|
||||
|
||||
// editAction creates an infinity loop.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
actionConnector,
|
||||
index,
|
||||
actionParams.subActionParams?.caseId,
|
||||
actionParams.subActionParams?.comment,
|
||||
caseId,
|
||||
comment,
|
||||
actionParams.subAction,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut size="s" title={i18n.CASE_CONNECTOR_CALL_OUT_INFO} iconType="iInCircle" />
|
||||
<Container>
|
||||
<ExistingCase onCaseChanged={onCaseChanged} selectedCase={selectedCase} />
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { CaseParamsFields as default };
|
|
@ -3,11 +3,29 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { lazy } from 'react';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionTypeModel } from '../../../../../../triggers_actions_ui/public/types';
|
||||
import { CaseActionParams } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ValidationResult {
|
||||
errors: {
|
||||
caseId: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const validateParams = (actionParams: CaseActionParams) => {
|
||||
const validationResult: ValidationResult = { errors: { caseId: [] } };
|
||||
|
||||
if (actionParams.subActionParams && !actionParams.subActionParams.caseId) {
|
||||
validationResult.errors.caseId.push(i18n.CASE_CONNECTOR_CASE_REQUIRED);
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
};
|
||||
|
||||
export function getActionType(): ActionTypeModel {
|
||||
return {
|
||||
id: '.case',
|
||||
|
@ -15,8 +33,8 @@ export function getActionType(): ActionTypeModel {
|
|||
selectMessage: i18n.CASE_CONNECTOR_DESC,
|
||||
actionTypeTitle: i18n.CASE_CONNECTOR_TITLE,
|
||||
validateConnector: () => ({ errors: {} }),
|
||||
validateParams: () => ({ errors: {} }),
|
||||
validateParams,
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: null,
|
||||
actionParamsFields: lazy(() => import('./fields')),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 * from '../../../translations';
|
||||
|
||||
export const CASE_CONNECTOR_DESC = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create or update a case.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Cases',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_COMMENT_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.commentLabel',
|
||||
{
|
||||
defaultMessage: 'Comment',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.commentRequired',
|
||||
{
|
||||
defaultMessage: 'Comment is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel',
|
||||
{
|
||||
defaultMessage: 'Case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_CASES_DROPDOWN_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_CASES_OPTION_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.optionAddNewCase',
|
||||
{
|
||||
defaultMessage: 'Add to a new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_CASES_OPTION_EXISTING_CASE = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.optionAddToExistingCase',
|
||||
{
|
||||
defaultMessage: 'Add to existing case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.caseRequired',
|
||||
{
|
||||
defaultMessage: 'You must select a case.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_CALL_OUT_INFO = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.callOutInfo',
|
||||
{
|
||||
defaultMessage: 'All alerts after rule creation will be appended to the selected case.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_ADD_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.addNewCaseOption',
|
||||
{
|
||||
defaultMessage: 'Add new case',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface CaseActionParams {
|
||||
subAction: string;
|
||||
subActionParams: {
|
||||
caseId: string;
|
||||
comment: {
|
||||
alertId: string;
|
||||
index: string;
|
||||
type: 'alert';
|
||||
};
|
||||
};
|
||||
}
|
|
@ -5,3 +5,7 @@
|
|||
*/
|
||||
|
||||
export { getActionType as getCaseConnectorUI } from './case';
|
||||
|
||||
export * from './config';
|
||||
export * from './types';
|
||||
export * from './utils';
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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 { act, waitFor } from '@testing-library/react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { useForm, Form, FormHook } from '../../../shared_imports';
|
||||
import { connectorsMock } from '../../containers/mock';
|
||||
import { Connector } from './connector';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types';
|
||||
import { useGetSeverity } from '../settings/resilient/use_get_severity';
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
return {
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
notifications: {},
|
||||
http: {},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
jest.mock('../settings/resilient/use_get_incident_types');
|
||||
jest.mock('../settings/resilient/use_get_severity');
|
||||
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
|
||||
const useGetSeverityMock = useGetSeverity as jest.Mock;
|
||||
|
||||
const useGetIncidentTypesResponse = {
|
||||
isLoading: false,
|
||||
incidentTypes: [
|
||||
{
|
||||
id: 19,
|
||||
name: 'Malware',
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: 'Denial of Service',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useGetSeverityResponse = {
|
||||
isLoading: false,
|
||||
severity: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Connector', () => {
|
||||
let globalForm: FormHook;
|
||||
|
||||
const MockHookWrapperComponent: React.FC = ({ children }) => {
|
||||
const { form } = useForm<{ connectorId: string; fields: Record<string, unknown> | null }>({
|
||||
defaultValue: { connectorId: connectorsMock[0].id, fields: null },
|
||||
});
|
||||
|
||||
globalForm = form;
|
||||
|
||||
return <Form form={form}>{children}</Form>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock });
|
||||
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
|
||||
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
|
||||
});
|
||||
|
||||
it('it renders', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Connector isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="connector-settings"]`).exists()).toBeTruthy();
|
||||
|
||||
waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('it is loading when fetching connectors', async () => {
|
||||
useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock });
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Connector isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading')
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('it is disabled when fetching connectors', async () => {
|
||||
useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock });
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Connector isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('it is disabled and loading when passing loading as true', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Connector isLoading={true} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading')
|
||||
).toEqual(true);
|
||||
expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it(`it should change connector`, async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Connector isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
((wrapper.find(EuiComboBox).props() as unknown) as {
|
||||
onChange: (a: EuiComboBoxOptionOption[]) => void;
|
||||
}).onChange([{ value: '19', label: 'Denial of Service' }]);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('select[data-test-subj="severitySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '4' },
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(globalForm.getFormData()).toEqual({
|
||||
connectorId: 'resilient-2',
|
||||
fields: { incidentTypes: ['19'], severityCode: '4' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { ConnectorTypeFields } from '../../../../../case/common/api/connectors';
|
||||
import { UseField, useFormData, FieldHook } from '../../../shared_imports';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { ConnectorSelector } from '../connector_selector/form';
|
||||
import { SettingFieldsForm } from '../settings/fields_form';
|
||||
import { ActionConnector } from '../../containers/types';
|
||||
import { getConnectorById } from '../configure_cases/utils';
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
interface SettingsFieldProps {
|
||||
connectors: ActionConnector[];
|
||||
field: FieldHook<ConnectorTypeFields['fields']>;
|
||||
isEdit: boolean;
|
||||
}
|
||||
|
||||
const SettingsField = ({ connectors, isEdit, field }: SettingsFieldProps) => {
|
||||
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
|
||||
const { setValue } = field;
|
||||
const connector = getConnectorById(connectorId, connectors) ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (connectorId) {
|
||||
setValue(null);
|
||||
}
|
||||
}, [setValue, connectorId]);
|
||||
|
||||
return (
|
||||
<SettingFieldsForm
|
||||
connector={connector}
|
||||
fields={field.value}
|
||||
isEdit={isEdit}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectorComponent: React.FC<Props> = ({ isLoading }) => {
|
||||
const { loading: isLoadingConnectors, connectors } = useConnectors();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="connectorId"
|
||||
component={ConnectorSelector}
|
||||
componentProps={{
|
||||
connectors,
|
||||
dataTestSubj: 'caseConnectors',
|
||||
disabled: isLoading || isLoadingConnectors,
|
||||
idAria: 'caseConnectors',
|
||||
isLoading: isLoading || isLoadingConnectors,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="fields"
|
||||
component={SettingsField}
|
||||
componentProps={{
|
||||
connectors,
|
||||
isEdit: true,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
ConnectorComponent.displayName = 'ConnectorComponent';
|
||||
|
||||
export const Connector = memo(ConnectorComponent);
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { act } from '@testing-library/react';
|
||||
|
||||
import { useForm, Form, FormHook } from '../../../shared_imports';
|
||||
import { Description } from './description';
|
||||
|
||||
describe('Description', () => {
|
||||
let globalForm: FormHook;
|
||||
|
||||
const MockHookWrapperComponent: React.FC = ({ children }) => {
|
||||
const { form } = useForm<{ description: string }>({
|
||||
defaultValue: { description: 'My description' },
|
||||
});
|
||||
|
||||
globalForm = form;
|
||||
|
||||
return <Form form={form}>{children}</Form>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('it renders', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Description isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it changes the description', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Description isLoading={true} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find(`[data-test-subj="caseDescription"] textarea`)
|
||||
.first()
|
||||
.simulate('change', { target: { value: 'My new description' } });
|
||||
});
|
||||
|
||||
expect(globalForm.getFormData()).toEqual({ description: 'My new description' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form';
|
||||
import { UseField } from '../../../shared_imports';
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const fieldName = 'description';
|
||||
|
||||
const DescriptionComponent: React.FC<Props> = ({ isLoading }) => (
|
||||
<UseField
|
||||
path={fieldName}
|
||||
component={MarkdownEditorForm}
|
||||
componentProps={{
|
||||
dataTestSubj: 'caseDescription',
|
||||
idAria: 'caseDescription',
|
||||
isDisabled: isLoading,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
DescriptionComponent.displayName = 'DescriptionComponent';
|
||||
|
||||
export const Description = memo(DescriptionComponent);
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { useForm, Form } from '../../../shared_imports';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { connectorsMock } from '../../containers/mock';
|
||||
import { schema, FormProps } from './schema';
|
||||
import { CreateCaseForm } from './form';
|
||||
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
const useGetTagsMock = useGetTags as jest.Mock;
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
|
||||
const initialCaseValue: FormProps = {
|
||||
description: '',
|
||||
tags: [],
|
||||
title: '',
|
||||
connectorId: 'none',
|
||||
fields: null,
|
||||
};
|
||||
|
||||
describe('CreateCaseForm', () => {
|
||||
const MockHookWrapperComponent: React.FC = ({ children }) => {
|
||||
const { form } = useForm<FormProps>({
|
||||
defaultValue: initialCaseValue,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
|
||||
return <Form form={form}>{children}</Form>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useGetTagsMock.mockReturnValue({ tags: ['test'] });
|
||||
useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock });
|
||||
});
|
||||
|
||||
it('it renders with steps', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it renders without steps', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm withSteps={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { EuiLoadingSpinner, EuiSteps } from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { useFormContext } from '../../../shared_imports';
|
||||
|
||||
import { Title } from './title';
|
||||
import { Description } from './description';
|
||||
import { Tags } from './tags';
|
||||
import { Connector } from './connector';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ContainerProps {
|
||||
big?: boolean;
|
||||
}
|
||||
|
||||
const Container = styled.div.attrs((props) => props)<ContainerProps>`
|
||||
${({ big, theme }) => css`
|
||||
margin-top: ${big ? theme.eui?.euiSizeXL ?? '32px' : theme.eui?.euiSize ?? '16px'};
|
||||
`}
|
||||
`;
|
||||
|
||||
const MySpinner = styled(EuiLoadingSpinner)`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 99;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
withSteps?: boolean;
|
||||
}
|
||||
|
||||
export const CreateCaseForm: React.FC<Props> = React.memo(({ withSteps = true }) => {
|
||||
const { isSubmitting } = useFormContext();
|
||||
|
||||
const firstStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_ONE_TITLE,
|
||||
children: (
|
||||
<>
|
||||
<Title isLoading={isSubmitting} />
|
||||
<Container>
|
||||
<Tags isLoading={isSubmitting} />
|
||||
</Container>
|
||||
<Container big>
|
||||
<Description isLoading={isSubmitting} />
|
||||
</Container>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[isSubmitting]
|
||||
);
|
||||
|
||||
const secondStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_TWO_TITLE,
|
||||
children: (
|
||||
<Container>
|
||||
<Connector isLoading={isSubmitting} />
|
||||
</Container>
|
||||
),
|
||||
}),
|
||||
[isSubmitting]
|
||||
);
|
||||
|
||||
const allSteps = useMemo(() => [firstStep, secondStep], [firstStep, secondStep]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />}
|
||||
{withSteps ? (
|
||||
<EuiSteps
|
||||
headingElement="h2"
|
||||
steps={allSteps}
|
||||
data-test-subj={'case-creation-form-steps'}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{firstStep.children}
|
||||
{secondStep.children}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
CreateCaseForm.displayName = 'CreateCaseForm';
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
|
||||
import { schema, FormProps } from './schema';
|
||||
import { Form, useForm } from '../../../shared_imports';
|
||||
import {
|
||||
getConnectorById,
|
||||
getNoneConnector,
|
||||
normalizeActionConnector,
|
||||
} from '../configure_cases/utils';
|
||||
import { usePostCase } from '../../containers/use_post_case';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { Case } from '../../containers/types';
|
||||
|
||||
const initialCaseValue: FormProps = {
|
||||
description: '',
|
||||
tags: [],
|
||||
title: '',
|
||||
connectorId: 'none',
|
||||
fields: null,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onSuccess?: (theCase: Case) => void;
|
||||
}
|
||||
|
||||
export const FormContext: React.FC<Props> = ({ children, onSuccess }) => {
|
||||
const { connectors } = useConnectors();
|
||||
const { caseData, postCase } = usePostCase();
|
||||
|
||||
const submitCase = useCallback(
|
||||
async ({ connectorId: dataConnectorId, fields, ...dataWithoutConnectorId }, isValid) => {
|
||||
if (isValid) {
|
||||
const caseConnector = getConnectorById(dataConnectorId, connectors);
|
||||
const connectorToUpdate = caseConnector
|
||||
? normalizeActionConnector(caseConnector, fields)
|
||||
: getNoneConnector();
|
||||
|
||||
await postCase({ ...dataWithoutConnectorId, connector: connectorToUpdate });
|
||||
}
|
||||
},
|
||||
[postCase, connectors]
|
||||
);
|
||||
|
||||
const { form } = useForm<FormProps>({
|
||||
defaultValue: initialCaseValue,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
onSubmit: submitCase,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (caseData && onSuccess) {
|
||||
onSuccess(caseData);
|
||||
}
|
||||
}, [caseData, onSuccess]);
|
||||
|
||||
return <Form form={form}>{children}</Form>;
|
||||
};
|
||||
|
||||
FormContext.displayName = 'FormContext';
|
|
@ -5,71 +5,40 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Create } from '.';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { getFormMock } from '../__mock__/form';
|
||||
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
|
||||
|
||||
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
|
||||
import { usePostCase } from '../../containers/use_post_case';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
|
||||
import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form';
|
||||
import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data';
|
||||
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { connectorsMock } from '../../containers/configure/mock';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
|
||||
import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types';
|
||||
import { useGetSeverity } from '../settings/resilient/use_get_severity';
|
||||
import { useGetIssueTypes } from '../settings/jira/use_get_issue_types';
|
||||
import { useGetFieldsByIssueType } from '../settings/jira/use_get_fields_by_issue_type';
|
||||
import { Create } from '.';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
return {
|
||||
...original,
|
||||
// eslint-disable-next-line react/display-name
|
||||
EuiFieldText: () => <input />,
|
||||
};
|
||||
});
|
||||
jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline');
|
||||
jest.mock('../../containers/use_post_case');
|
||||
|
||||
jest.mock(
|
||||
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'
|
||||
);
|
||||
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
jest.mock(
|
||||
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider',
|
||||
() => ({
|
||||
FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) =>
|
||||
children({ tags: ['rad', 'dude'] }),
|
||||
})
|
||||
);
|
||||
jest.mock('../settings/resilient/use_get_incident_types');
|
||||
jest.mock('../settings/resilient/use_get_severity');
|
||||
jest.mock('../settings/jira/use_get_issue_types');
|
||||
jest.mock('../settings/jira/use_get_fields_by_issue_type');
|
||||
jest.mock('../settings/jira/use_get_single_issue');
|
||||
jest.mock('../settings/jira/use_get_issues');
|
||||
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
const useFormMock = useForm as jest.Mock;
|
||||
const useFormDataMock = useFormData as jest.Mock;
|
||||
|
||||
const useInsertTimelineMock = useInsertTimeline as jest.Mock;
|
||||
const usePostCaseMock = usePostCase as jest.Mock;
|
||||
|
||||
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
|
||||
const useGetSeverityMock = useGetSeverity as jest.Mock;
|
||||
const useGetIssueTypesMock = useGetIssueTypes as jest.Mock;
|
||||
const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock;
|
||||
const postCase = jest.fn();
|
||||
const handleCursorChange = jest.fn();
|
||||
const handleOnTimelineChange = jest.fn();
|
||||
|
||||
const defaultInsertTimeline = {
|
||||
cursorPosition: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
handleCursorChange,
|
||||
handleOnTimelineChange,
|
||||
};
|
||||
|
||||
const sampleTags = ['coke', 'pepsi'];
|
||||
const sampleData = {
|
||||
|
@ -83,27 +52,117 @@ const sampleData = {
|
|||
type: ConnectorTypes.none,
|
||||
},
|
||||
};
|
||||
|
||||
const defaultPostCase = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
caseData: null,
|
||||
postCase,
|
||||
};
|
||||
|
||||
const sampleConnectorData = { loading: false, connectors: [] };
|
||||
|
||||
const useGetIncidentTypesResponse = {
|
||||
isLoading: false,
|
||||
incidentTypes: [
|
||||
{
|
||||
id: 19,
|
||||
name: 'Malware',
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: 'Denial of Service',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useGetSeverityResponse = {
|
||||
isLoading: false,
|
||||
severity: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useGetIssueTypesResponse = {
|
||||
isLoading: false,
|
||||
issueTypes: [
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Bug',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useGetFieldsByIssueTypeResponse = {
|
||||
isLoading: false,
|
||||
fields: {
|
||||
summary: { allowedValues: [], defaultValue: {} },
|
||||
labels: { allowedValues: [], defaultValue: {} },
|
||||
description: { allowedValues: [], defaultValue: {} },
|
||||
priority: {
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
defaultValue: { name: 'Medium', id: '3' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const fillForm = async (wrapper: ReactWrapper) => {
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find(`[data-test-subj="caseTitle"] input`)
|
||||
.first()
|
||||
.simulate('change', { target: { value: sampleData.title } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find(`[data-test-subj="caseDescription"] textarea`)
|
||||
.first()
|
||||
.simulate('change', { target: { value: sampleData.description } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
((wrapper.find(EuiComboBox).props() as unknown) as {
|
||||
onChange: (a: EuiComboBoxOptionOption[]) => void;
|
||||
}).onChange(sampleTags.map((tag) => ({ label: tag })));
|
||||
});
|
||||
};
|
||||
|
||||
describe('Create case', () => {
|
||||
const fetchTags = jest.fn();
|
||||
const formHookMock = getFormMock(sampleData);
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline);
|
||||
usePostCaseMock.mockImplementation(() => defaultPostCase);
|
||||
useFormMock.mockImplementation(() => ({ form: formHookMock }));
|
||||
useFormDataMock.mockImplementation(() => [
|
||||
{
|
||||
description: sampleData.description,
|
||||
},
|
||||
]);
|
||||
useConnectorsMock.mockReturnValue(sampleConnectorData);
|
||||
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
|
||||
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
|
||||
useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
|
||||
useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse);
|
||||
|
||||
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
|
||||
(useGetTags as jest.Mock).mockImplementation(() => ({
|
||||
tags: sampleTags,
|
||||
|
@ -112,7 +171,7 @@ describe('Create case', () => {
|
|||
});
|
||||
|
||||
describe('Step 1 - Case Fields', () => {
|
||||
it('should post case on submit click', async () => {
|
||||
it('it renders', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
|
@ -120,7 +179,38 @@ describe('Create case', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
|
||||
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="create-case-submit"]`).first().exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="create-case-cancel"]`).first().exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-creation-form-steps"]`).first().exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should post case on submit click', async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await fillForm(wrapper);
|
||||
wrapper.update();
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
});
|
||||
await waitFor(() => expect(postCase).toBeCalledWith(sampleData));
|
||||
});
|
||||
|
||||
|
@ -132,15 +222,18 @@ describe('Create case', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click');
|
||||
await waitFor(() => expect(mockHistory.push).toHaveBeenCalledWith('/'));
|
||||
});
|
||||
|
||||
it('should redirect to new case when caseData is there', async () => {
|
||||
const sampleId = '777777';
|
||||
const sampleId = 'case-id';
|
||||
usePostCaseMock.mockImplementation(() => ({
|
||||
...defaultPostCase,
|
||||
caseData: { id: sampleId },
|
||||
}));
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
|
@ -148,11 +241,11 @@ describe('Create case', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/777777'));
|
||||
|
||||
await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/case-id'));
|
||||
});
|
||||
|
||||
it('should render spinner when loading', async () => {
|
||||
usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true }));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
|
@ -160,75 +253,197 @@ describe('Create case', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy()
|
||||
);
|
||||
});
|
||||
it('Tag options render with new tags added', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() =>
|
||||
|
||||
await fillForm(wrapper);
|
||||
await act(async () => {
|
||||
await wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="caseTags"] [data-test-subj="input"]`)
|
||||
.first()
|
||||
.prop('options')
|
||||
).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// FAILED ES PROMOTION: https://github.com/elastic/kibana/issues/84145
|
||||
describe.skip('Step 2 - Connector Fields', () => {
|
||||
const connectorTypes = [
|
||||
{
|
||||
label: 'Jira',
|
||||
testId: 'jira-1',
|
||||
dataTestSubj: 'connector-settings-jira',
|
||||
},
|
||||
{
|
||||
label: 'Resilient',
|
||||
testId: 'resilient-2',
|
||||
dataTestSubj: 'connector-settings-resilient',
|
||||
},
|
||||
{
|
||||
label: 'ServiceNow',
|
||||
testId: 'servicenow-1',
|
||||
dataTestSubj: 'connector-settings-sn',
|
||||
},
|
||||
];
|
||||
connectorTypes.forEach(({ label, testId, dataTestSubj }) => {
|
||||
it(`should change from none to ${label} connector fields`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="${dataTestSubj}"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-${testId}"]`).simulate('click');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="${dataTestSubj}"]`).exists()).toBeTruthy();
|
||||
});
|
||||
wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step 2 - Connector Fields', () => {
|
||||
it(`it should submit a Jira connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await fillForm(wrapper);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="connector-settings-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');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('select[data-test-subj="issueTypeSelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '10007' },
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('select[data-test-subj="prioritySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '2' },
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(postCase).toBeCalledWith({
|
||||
...sampleData,
|
||||
connector: {
|
||||
id: 'jira-1',
|
||||
name: 'Jira',
|
||||
type: '.jira',
|
||||
fields: { issueType: '10007', parent: null, priority: '2' },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`it should submit a resilient connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await fillForm(wrapper);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()
|
||||
).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
((wrapper.find(EuiComboBox).at(1).props() as unknown) as {
|
||||
onChange: (a: EuiComboBoxOptionOption[]) => void;
|
||||
}).onChange([{ value: '19', label: 'Denial of Service' }]);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('select[data-test-subj="severitySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '4' },
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(postCase).toBeCalledWith({
|
||||
...sampleData,
|
||||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'My Connector 2',
|
||||
type: '.resilient',
|
||||
fields: { incidentTypes: ['19'], severityCode: '4' },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`it should submit a servicenow connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await fillForm(wrapper);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find(`select[data-test-subj="${subj}"]`)
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '2' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(postCase).toBeCalledWith({
|
||||
...sampleData,
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'My Connector',
|
||||
type: '.servicenow',
|
||||
fields: { impact: '2', severity: '2', urgency: '2' },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,319 +3,81 @@
|
|||
* 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, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSteps,
|
||||
} from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
|
||||
import {
|
||||
Field,
|
||||
Form,
|
||||
FormDataProvider,
|
||||
getUseField,
|
||||
UseField,
|
||||
useForm,
|
||||
useFormData,
|
||||
} from '../../../shared_imports';
|
||||
import { usePostCase } from '../../containers/use_post_case';
|
||||
import { schema, FormProps } from './schema';
|
||||
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
|
||||
import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Field, getUseField, useFormContext } from '../../../shared_imports';
|
||||
import { getCaseDetailsUrl } from '../../../common/components/link_to';
|
||||
import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click';
|
||||
import { SettingFieldsForm } from '../settings/fields_form';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { ConnectorSelector } from '../connector_selector/form';
|
||||
import { useCaseConfigure } from '../../containers/configure/use_configure';
|
||||
import {
|
||||
normalizeCaseConnector,
|
||||
getConnectorById,
|
||||
getNoneConnector,
|
||||
normalizeActionConnector,
|
||||
} from '../configure_cases/utils';
|
||||
import { ActionConnector } from '../../containers/types';
|
||||
import { ConnectorFields } from '../../../../../case/common/api/connectors';
|
||||
import * as i18n from './translations';
|
||||
import { CreateCaseForm } from './form';
|
||||
import { FormContext } from './form_context';
|
||||
import { useInsertTimeline } from '../use_insert_timeline';
|
||||
import { fieldName as descriptionFieldName } from './description';
|
||||
import { SubmitCaseButton } from './submit_button';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface ContainerProps {
|
||||
big?: boolean;
|
||||
}
|
||||
|
||||
const Container = styled.div.attrs((props) => props)<ContainerProps>`
|
||||
${({ big, theme }) => css`
|
||||
margin-top: ${big ? theme.eui.euiSizeXL : theme.eui.euiSize};
|
||||
const Container = styled.div`
|
||||
${({ theme }) => `
|
||||
margin-top: ${theme.eui.euiSize};
|
||||
`}
|
||||
`;
|
||||
|
||||
const MySpinner = styled(EuiLoadingSpinner)`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 99;
|
||||
`;
|
||||
|
||||
const initialCaseValue: FormProps = {
|
||||
description: '',
|
||||
tags: [],
|
||||
title: '',
|
||||
connectorId: 'none',
|
||||
const InsertTimeline = () => {
|
||||
const { setFieldValue, getFormData } = useFormContext();
|
||||
const formData = getFormData();
|
||||
const onTimelineAttached = useCallback(
|
||||
(newValue: string) => setFieldValue(descriptionFieldName, newValue),
|
||||
[setFieldValue]
|
||||
);
|
||||
useInsertTimeline(formData[descriptionFieldName] ?? '', onTimelineAttached);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Create = React.memo(() => {
|
||||
const history = useHistory();
|
||||
const { caseData, isLoading, postCase } = usePostCase();
|
||||
const { loading: isLoadingConnectors, connectors } = useConnectors();
|
||||
const { connector: configureConnector, loading: isLoadingCaseConfigure } = useCaseConfigure();
|
||||
const { tags: tagOptions } = useGetTags();
|
||||
|
||||
const [connector, setConnector] = useState<ActionConnector | null>(null);
|
||||
const [options, setOptions] = useState(
|
||||
tagOptions.map((label) => ({
|
||||
label,
|
||||
}))
|
||||
);
|
||||
|
||||
// This values uses useEffect to update, not useMemo,
|
||||
// because we need to setState on it from the jsx
|
||||
useEffect(
|
||||
() =>
|
||||
setOptions(
|
||||
tagOptions.map((label) => ({
|
||||
label,
|
||||
}))
|
||||
),
|
||||
[tagOptions]
|
||||
);
|
||||
|
||||
const [fields, setFields] = useState<ConnectorFields>(null);
|
||||
|
||||
const { form } = useForm<FormProps>({
|
||||
defaultValue: initialCaseValue,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
const currentConnectorId = useMemo(
|
||||
() =>
|
||||
!isLoadingCaseConfigure
|
||||
? normalizeCaseConnector(connectors, configureConnector)?.id ?? 'none'
|
||||
: null,
|
||||
[configureConnector, connectors, isLoadingCaseConfigure]
|
||||
);
|
||||
const { submit, setFieldValue } = form;
|
||||
const [{ description }] = useFormData<{
|
||||
description: string;
|
||||
}>({
|
||||
form,
|
||||
watch: ['description'],
|
||||
});
|
||||
const onChangeConnector = useCallback(
|
||||
(newConnectorId) => {
|
||||
if (connector == null || connector.id !== newConnectorId) {
|
||||
setConnector(getConnectorById(newConnectorId, connectors) ?? null);
|
||||
// Reset setting fields when changing connector
|
||||
setFields(null);
|
||||
}
|
||||
const onSuccess = useCallback(
|
||||
({ id }) => {
|
||||
history.push(getCaseDetailsUrl({ id }));
|
||||
},
|
||||
[connector, connectors]
|
||||
[history]
|
||||
);
|
||||
|
||||
const onDescriptionChange = useCallback((newValue) => setFieldValue('description', newValue), [
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
const { handleCursorChange } = useInsertTimeline(description, onDescriptionChange);
|
||||
|
||||
const handleTimelineClick = useTimelineClick();
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
const { isValid, data } = await submit();
|
||||
if (isValid) {
|
||||
const { connectorId: dataConnectorId, ...dataWithoutConnectorId } = data;
|
||||
const caseConnector = getConnectorById(dataConnectorId, connectors);
|
||||
const connectorToUpdate = caseConnector
|
||||
? normalizeActionConnector(caseConnector, fields)
|
||||
: getNoneConnector();
|
||||
|
||||
await postCase({ ...dataWithoutConnectorId, connector: connectorToUpdate });
|
||||
}
|
||||
}, [submit, postCase, fields, connectors]);
|
||||
|
||||
const handleSetIsCancel = useCallback(() => {
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
|
||||
const firstStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_ONE_TITLE,
|
||||
children: (
|
||||
<>
|
||||
<CommonUseField
|
||||
path="title"
|
||||
componentProps={{
|
||||
idAria: 'caseTitle',
|
||||
'data-test-subj': 'caseTitle',
|
||||
euiFieldProps: {
|
||||
fullWidth: false,
|
||||
disabled: isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Container>
|
||||
<CommonUseField
|
||||
path="tags"
|
||||
componentProps={{
|
||||
idAria: 'caseTags',
|
||||
'data-test-subj': 'caseTags',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
placeholder: '',
|
||||
disabled: isLoading,
|
||||
options,
|
||||
noSuggestions: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FormDataProvider pathsToWatch="tags">
|
||||
{({ tags: anotherTags }) => {
|
||||
const current: string[] = options.map((opt) => opt.label);
|
||||
const newOptions = anotherTags.reduce((acc: string[], item: string) => {
|
||||
if (!acc.includes(item)) {
|
||||
return [...acc, item];
|
||||
}
|
||||
return acc;
|
||||
}, current);
|
||||
if (!isEqual(current, newOptions)) {
|
||||
setOptions(
|
||||
newOptions.map((label: string) => ({
|
||||
label,
|
||||
}))
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</Container>
|
||||
<Container big>
|
||||
<UseField
|
||||
path={'description'}
|
||||
component={MarkdownEditorForm}
|
||||
componentProps={{
|
||||
dataTestSubj: 'caseDescription',
|
||||
idAria: 'caseDescription',
|
||||
isDisabled: isLoading,
|
||||
onClickTimeline: handleTimelineClick,
|
||||
onCursorPositionUpdate: handleCursorChange,
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[isLoading, options, handleCursorChange, handleTimelineClick]
|
||||
);
|
||||
|
||||
const secondStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_TWO_TITLE,
|
||||
children: (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<Container>
|
||||
<UseField
|
||||
path="connectorId"
|
||||
component={ConnectorSelector}
|
||||
componentProps={{
|
||||
connectors,
|
||||
dataTestSubj: 'caseConnectors',
|
||||
defaultValue: currentConnectorId,
|
||||
disabled: isLoadingConnectors,
|
||||
idAria: 'caseConnectors',
|
||||
isLoading,
|
||||
}}
|
||||
onChange={onChangeConnector}
|
||||
/>
|
||||
</Container>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Container>
|
||||
<SettingFieldsForm
|
||||
connector={connector}
|
||||
fields={fields}
|
||||
isEdit={true}
|
||||
onChange={setFields}
|
||||
/>
|
||||
</Container>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}),
|
||||
[
|
||||
connector,
|
||||
connectors,
|
||||
currentConnectorId,
|
||||
fields,
|
||||
isLoading,
|
||||
isLoadingConnectors,
|
||||
onChangeConnector,
|
||||
]
|
||||
);
|
||||
|
||||
const allSteps = useMemo(() => [firstStep, secondStep], [firstStep, secondStep]);
|
||||
|
||||
if (caseData != null && caseData.id) {
|
||||
history.push(getCaseDetailsUrl({ id: caseData.id }));
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
{isLoading && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />}
|
||||
<Form form={form}>
|
||||
<EuiSteps headingElement="h2" steps={allSteps} />
|
||||
</Form>
|
||||
<Container>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="create-case-cancel"
|
||||
size="s"
|
||||
onClick={handleSetIsCancel}
|
||||
iconType="cross"
|
||||
>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="create-case-submit"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{i18n.CREATE_CASE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Container>
|
||||
<FormContext onSuccess={onSuccess}>
|
||||
<CreateCaseForm />
|
||||
<Container>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="create-case-cancel"
|
||||
size="s"
|
||||
onClick={handleSetIsCancel}
|
||||
iconType="cross"
|
||||
>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SubmitCaseButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Container>
|
||||
<InsertTimeline />
|
||||
</FormContext>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { OptionalFieldLabel } from '.';
|
||||
|
||||
describe('OptionalFieldLabel', () => {
|
||||
it('it renders correctly', async () => {
|
||||
const wrapper = mount(OptionalFieldLabel);
|
||||
expect(wrapper.find('[data-test-subj="form-optional-field-label"]').first().text()).toBe(
|
||||
'Optional'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import * as i18n from '../../../translations';
|
||||
|
||||
export const OptionalFieldLabel = (
|
||||
<EuiText color="subdued" size="xs">
|
||||
<EuiText color="subdued" size="xs" data-test-subj="form-optional-field-label">
|
||||
{i18n.OPTIONAL}
|
||||
</EuiText>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CasePostRequest } from '../../../../../case/common/api';
|
||||
import { CasePostRequest, ConnectorTypeFields } from '../../../../../case/common/api';
|
||||
import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
|
@ -18,7 +18,10 @@ export const schemaTags = {
|
|||
labelAppend: OptionalFieldLabel,
|
||||
};
|
||||
|
||||
export type FormProps = Omit<CasePostRequest, 'connector'> & { connectorId: string };
|
||||
export type FormProps = Omit<CasePostRequest, 'connector'> & {
|
||||
connectorId: string;
|
||||
fields: ConnectorTypeFields['fields'];
|
||||
};
|
||||
|
||||
export const schema: FormSchema<FormProps> = {
|
||||
title: {
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useForm, Form } from '../../../shared_imports';
|
||||
import { SubmitCaseButton } from './submit_button';
|
||||
|
||||
describe('SubmitCaseButton', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
const MockHookWrapperComponent: React.FC = ({ children }) => {
|
||||
const { form } = useForm<{ title: string }>({
|
||||
defaultValue: { title: 'My title' },
|
||||
onSubmit,
|
||||
});
|
||||
|
||||
return <Form form={form}>{children}</Form>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('it renders', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<SubmitCaseButton />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it submits', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<SubmitCaseButton />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() => expect(onSubmit).toBeCalled());
|
||||
});
|
||||
|
||||
it('it disables when submitting', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<SubmitCaseButton />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isDisabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('it is loading when submitting', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<SubmitCaseButton />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isLoading')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
|
||||
import { useFormContext } from '../../../shared_imports';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const SubmitCaseButtonComponent: React.FC = () => {
|
||||
const { submit, isSubmitting } = useFormContext();
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
data-test-subj="create-case-submit"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={submit}
|
||||
>
|
||||
{i18n.CREATE_CASE}
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const SubmitCaseButton = memo(SubmitCaseButtonComponent);
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { useForm, Form, FormHook, FIELD_TYPES } from '../../../shared_imports';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { Tags } from './tags';
|
||||
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
const useGetTagsMock = useGetTags as jest.Mock;
|
||||
|
||||
describe('Tags', () => {
|
||||
let globalForm: FormHook;
|
||||
|
||||
const MockHookWrapperComponent: React.FC = ({ children }) => {
|
||||
const { form } = useForm<{ tags: string[] }>({
|
||||
defaultValue: { tags: [] },
|
||||
schema: {
|
||||
tags: { type: FIELD_TYPES.COMBO_BOX },
|
||||
},
|
||||
});
|
||||
|
||||
globalForm = form;
|
||||
|
||||
return <Form form={form}>{children}</Form>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useGetTagsMock.mockReturnValue({ tags: ['test'] });
|
||||
});
|
||||
|
||||
it('it renders', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Tags isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="caseTags"]`).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('it disables the input when loading', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Tags isLoading={true} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiComboBox).prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it changes the tags', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Tags isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
((wrapper.find(EuiComboBox).props() as unknown) as {
|
||||
onChange: (a: EuiComboBoxOptionOption[]) => void;
|
||||
}).onChange(['test', 'case'].map((tag) => ({ label: tag })));
|
||||
});
|
||||
|
||||
expect(globalForm.getFormData()).toEqual({ tags: ['test', 'case'] });
|
||||
});
|
||||
});
|
|
@ -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 React, { memo, useMemo } from 'react';
|
||||
|
||||
import { Field, getUseField } from '../../../shared_imports';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const TagsComponent: React.FC<Props> = ({ isLoading }) => {
|
||||
const { tags: tagOptions, isLoading: isLoadingTags } = useGetTags();
|
||||
const options = useMemo(
|
||||
() =>
|
||||
tagOptions.map((label) => ({
|
||||
label,
|
||||
})),
|
||||
[tagOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<CommonUseField
|
||||
path="tags"
|
||||
componentProps={{
|
||||
idAria: 'caseTags',
|
||||
'data-test-subj': 'caseTags',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
placeholder: '',
|
||||
disabled: isLoading || isLoadingTags,
|
||||
options,
|
||||
noSuggestions: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TagsComponent.displayName = 'TagsComponent';
|
||||
|
||||
export const Tags = memo(TagsComponent);
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { act } from '@testing-library/react';
|
||||
|
||||
import { useForm, Form, FormHook } from '../../../shared_imports';
|
||||
import { Title } from './title';
|
||||
|
||||
describe('Title', () => {
|
||||
let globalForm: FormHook;
|
||||
|
||||
const MockHookWrapperComponent: React.FC = ({ children }) => {
|
||||
const { form } = useForm<{ title: string }>({
|
||||
defaultValue: { title: 'My title' },
|
||||
});
|
||||
|
||||
globalForm = form;
|
||||
|
||||
return <Form form={form}>{children}</Form>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('it renders', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Title isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="caseTitle"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it disables the input when loading', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Title isLoading={true} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="caseTitle"] input`).prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it changes the title', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<Title isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find(`[data-test-subj="caseTitle"] input`)
|
||||
.first()
|
||||
.simulate('change', { target: { value: 'My new title' } });
|
||||
});
|
||||
|
||||
expect(globalForm.getFormData()).toEqual({ title: 'My new title' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { Field, getUseField } from '../../../shared_imports';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const TitleComponent: React.FC<Props> = ({ isLoading }) => (
|
||||
<CommonUseField
|
||||
path="title"
|
||||
componentProps={{
|
||||
idAria: 'caseTitle',
|
||||
'data-test-subj': 'caseTitle',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
disabled: isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
TitleComponent.displayName = 'TitleComponent';
|
||||
|
||||
export const Title = memo(TitleComponent);
|
|
@ -8,7 +8,7 @@ import React, { memo, useMemo } from 'react';
|
|||
import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { connectorsConfiguration } from '../../../common/lib/connectors/config';
|
||||
import { connectorsConfiguration } from '../connectors';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
|
||||
interface ConnectorCardProps {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { memo, Suspense, useCallback } from 'react';
|
||||
import React, { memo, Suspense } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { CaseSettingsConnector, SettingFieldsProps } from './types';
|
||||
|
@ -18,13 +18,6 @@ interface Props extends Omit<SettingFieldsProps<ConnectorTypeFields['fields']>,
|
|||
const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChange, fields }) => {
|
||||
const { caseSettingsRegistry } = getCaseSettings();
|
||||
|
||||
const onFieldsChange = useCallback(
|
||||
(newFields) => {
|
||||
onChange(newFields);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') {
|
||||
return null;
|
||||
}
|
||||
|
@ -45,12 +38,14 @@ const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChan
|
|||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<FieldsComponent
|
||||
isEdit={isEdit}
|
||||
fields={fields}
|
||||
connector={connector}
|
||||
onChange={onFieldsChange}
|
||||
/>
|
||||
<div data-test-subj={'connector-settings'}>
|
||||
<FieldsComponent
|
||||
isEdit={isEdit}
|
||||
fields={fields}
|
||||
connector={connector}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
@ -18,13 +18,14 @@ import {
|
|||
import styled, { css } from 'styled-components';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import * as i18n from './translations';
|
||||
import { Form, FormDataProvider, useForm } from '../../../shared_imports';
|
||||
import { Form, FormDataProvider, useForm, getUseField, Field } from '../../../shared_imports';
|
||||
import { schema } from './schema';
|
||||
import { CommonUseField } from '../create';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
|
||||
import { Tags } from './tags';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface TagListProps {
|
||||
disabled?: boolean;
|
||||
isLoading: boolean;
|
||||
|
|
|
@ -22,10 +22,7 @@ export interface AllCasesModalProps {
|
|||
onRowClick: (id?: string) => void;
|
||||
}
|
||||
|
||||
const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({
|
||||
onCloseCaseModal,
|
||||
onRowClick,
|
||||
}: AllCasesModalProps) => {
|
||||
const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({ onCloseCaseModal, onRowClick }) => {
|
||||
const userPermissions = useGetUserSavedObjectPermissions();
|
||||
const userCanCrud = userPermissions?.crud ?? false;
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormContext } from '../create/form_context';
|
||||
import { CreateCaseForm } from '../create/form';
|
||||
import { SubmitCaseButton } from '../create/submit_button';
|
||||
import { Case } from '../../containers/types';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
export interface CreateCaseModalProps {
|
||||
onCloseCaseModal: () => void;
|
||||
onCaseCreated: (theCase: Case) => void;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
${({ theme }) => `
|
||||
margin-top: ${theme.eui.euiSize};
|
||||
text-align: right;
|
||||
`}
|
||||
`;
|
||||
|
||||
const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
||||
onCloseCaseModal,
|
||||
onCaseCreated,
|
||||
}) => {
|
||||
const onSuccess = useCallback(
|
||||
(theCase) => {
|
||||
onCaseCreated(theCase);
|
||||
onCloseCaseModal();
|
||||
},
|
||||
[onCaseCreated, onCloseCaseModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask data-test-subj="all-cases-modal">
|
||||
<EuiModal onClose={onCloseCaseModal}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<FormContext onSuccess={onSuccess}>
|
||||
<CreateCaseForm withSteps={false} />
|
||||
<Container>
|
||||
<SubmitCaseButton />
|
||||
</Container>
|
||||
</FormContext>
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateCaseModal = memo(CreateModalComponent);
|
||||
|
||||
CreateCaseModal.displayName = 'CreateCaseModal';
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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, { useState, useCallback, useMemo } from 'react';
|
||||
import { Case } from '../../containers/types';
|
||||
import { CreateCaseModal } from './create_case_modal';
|
||||
|
||||
interface Props {
|
||||
onCaseCreated: (theCase: Case) => void;
|
||||
}
|
||||
export interface UseAllCasesModalReturnedValues {
|
||||
Modal: React.FC;
|
||||
isModalOpen: boolean;
|
||||
closeModal: () => void;
|
||||
openModal: () => void;
|
||||
}
|
||||
|
||||
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 state = useMemo(
|
||||
() => ({
|
||||
Modal,
|
||||
isModalOpen,
|
||||
closeModal,
|
||||
openModal,
|
||||
}),
|
||||
[isModalOpen, closeModal, openModal, Modal]
|
||||
);
|
||||
|
||||
return state;
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { useCallback, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
import { getTimelineUrl, useFormatUrl } from '../../../common/components/link_to';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { setInsertTimeline } from '../../../timelines/store/timeline/actions';
|
||||
|
||||
interface UseInsertTimelineReturn {
|
||||
handleOnTimelineChange: (title: string, id: string | null, graphEventId?: string) => void;
|
||||
}
|
||||
|
||||
export const useInsertTimeline = (
|
||||
value: string,
|
||||
onChange: (newValue: string) => void
|
||||
): UseInsertTimelineReturn => {
|
||||
const dispatch = useDispatch();
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.timelines);
|
||||
|
||||
const insertTimeline = useShallowEqualSelector(timelineSelectors.selectInsertTimeline);
|
||||
|
||||
const handleOnTimelineChange = useCallback(
|
||||
(title: string, id: string | null, graphEventId?: string) => {
|
||||
const url = formatUrl(getTimelineUrl(id ?? '', graphEventId), {
|
||||
absolute: true,
|
||||
skipSearch: true,
|
||||
});
|
||||
|
||||
let newValue = `[${title}](${url})`;
|
||||
// Leave a space between the previous value and the timeline url if the value is not empty.
|
||||
if (!isEmpty(value)) {
|
||||
newValue = `${value} ${newValue}`;
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
},
|
||||
[value, onChange, formatUrl]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (insertTimeline != null && value != null) {
|
||||
dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false }));
|
||||
handleOnTimelineChange(
|
||||
insertTimeline.timelineTitle,
|
||||
insertTimeline.timelineSavedObjectId,
|
||||
insertTimeline.graphEventId
|
||||
);
|
||||
dispatch(setInsertTimeline(null));
|
||||
}
|
||||
}, [insertTimeline, dispatch, handleOnTimelineChange, value]);
|
||||
|
||||
return {
|
||||
handleOnTimelineChange,
|
||||
};
|
||||
};
|
|
@ -1,21 +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 CASE_CONNECTOR_DESC = i18n.translate(
|
||||
'xpack.securitySolution.case.components.case.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create or update a case.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.case.components.case.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Cases',
|
||||
}
|
||||
);
|
|
@ -59,7 +59,7 @@ import {
|
|||
IndexFieldsStrategyResponse,
|
||||
} from '../common/search_strategy/index_fields';
|
||||
import { SecurityAppStore } from './common/store/store';
|
||||
import { getCaseConnectorUI } from './common/lib/connectors';
|
||||
import { getCaseConnectorUI } from './cases/components/connectors';
|
||||
import { licenseService } from './common/hooks/use_license';
|
||||
import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension';
|
||||
import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension';
|
||||
|
|
|
@ -1,70 +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 { useCallback, useState, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { SecurityPageName } from '../../../../../common/constants';
|
||||
import { getTimelineUrl, useFormatUrl } from '../../../../common/components/link_to';
|
||||
import { CursorPosition } from '../../../../common/components/markdown_editor';
|
||||
import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline';
|
||||
import { setInsertTimeline } from '../../../store/timeline/actions';
|
||||
|
||||
export const useInsertTimeline = (value: string, onChange: (newValue: string) => void) => {
|
||||
const dispatch = useDispatch();
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.timelines);
|
||||
const [cursorPosition, setCursorPosition] = useState<CursorPosition>({
|
||||
start: 0,
|
||||
end: 0,
|
||||
});
|
||||
|
||||
const insertTimeline = useShallowEqualSelector(timelineSelectors.selectInsertTimeline);
|
||||
|
||||
const handleOnTimelineChange = useCallback(
|
||||
(title: string, id: string | null, graphEventId?: string) => {
|
||||
const url = formatUrl(getTimelineUrl(id ?? '', graphEventId), {
|
||||
absolute: true,
|
||||
skipSearch: true,
|
||||
});
|
||||
|
||||
const newValue: string = [
|
||||
value.slice(0, cursorPosition.start),
|
||||
cursorPosition.start === cursorPosition.end
|
||||
? `[${title}](${url})`
|
||||
: `[${value.slice(cursorPosition.start, cursorPosition.end)}](${url})`,
|
||||
value.slice(cursorPosition.end),
|
||||
].join('');
|
||||
|
||||
onChange(newValue);
|
||||
},
|
||||
[value, onChange, cursorPosition, formatUrl]
|
||||
);
|
||||
|
||||
const handleCursorChange = useCallback((cp: CursorPosition) => {
|
||||
setCursorPosition(cp);
|
||||
}, []);
|
||||
|
||||
// insertTimeline selector is defined to attached a timeline to a case outside of the case page.
|
||||
// FYI, if you are in the case page we only use handleOnTimelineChange to attach a timeline to a case.
|
||||
useEffect(() => {
|
||||
if (insertTimeline != null && value != null) {
|
||||
dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false }));
|
||||
handleOnTimelineChange(
|
||||
insertTimeline.timelineTitle,
|
||||
insertTimeline.timelineSavedObjectId,
|
||||
insertTimeline.graphEventId
|
||||
);
|
||||
dispatch(setInsertTimeline(null));
|
||||
}
|
||||
}, [insertTimeline, dispatch, handleOnTimelineChange, value]);
|
||||
|
||||
return {
|
||||
cursorPosition,
|
||||
handleCursorChange,
|
||||
handleOnTimelineChange,
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue