mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SIEM] [Cases] Tags suggestions (#63878)
This commit is contained in:
parent
840808c942
commit
0c14424321
7 changed files with 184 additions and 13 deletions
|
@ -3,6 +3,10 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks';
|
||||
jest.mock(
|
||||
'../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
|
||||
);
|
||||
export const mockFormHook = {
|
||||
isSubmitted: false,
|
||||
isSubmitting: false,
|
||||
|
@ -35,3 +39,5 @@ export const getFormMock = (sampleData: any) => ({
|
|||
}),
|
||||
getFormData: () => sampleData,
|
||||
});
|
||||
|
||||
export const useFormMock = useForm as jest.Mock;
|
||||
|
|
|
@ -14,6 +14,8 @@ import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router
|
|||
|
||||
import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline';
|
||||
import { usePostCase } from '../../../../containers/case/use_post_case';
|
||||
import { useGetTags } from '../../../../containers/case/use_get_tags';
|
||||
|
||||
jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline');
|
||||
jest.mock('../../../../containers/case/use_post_case');
|
||||
import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks';
|
||||
|
@ -22,6 +24,14 @@ import { SiemPageName } from '../../../home/types';
|
|||
jest.mock(
|
||||
'../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
|
||||
);
|
||||
jest.mock('../../../../containers/case/use_get_tags');
|
||||
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'] }),
|
||||
})
|
||||
);
|
||||
|
||||
export const useFormMock = useForm as jest.Mock;
|
||||
|
||||
|
@ -40,9 +50,11 @@ const defaultInsertTimeline = {
|
|||
handleCursorChange,
|
||||
handleOnTimelineChange,
|
||||
};
|
||||
|
||||
const sampleTags = ['coke', 'pepsi'];
|
||||
const sampleData = {
|
||||
description: 'what a great description',
|
||||
tags: ['coke', 'pepsi'],
|
||||
tags: sampleTags,
|
||||
title: 'what a cool title',
|
||||
};
|
||||
const defaultPostCase = {
|
||||
|
@ -52,14 +64,28 @@ const defaultPostCase = {
|
|||
postCase,
|
||||
};
|
||||
describe('Create case', () => {
|
||||
// Suppress warnings about "noSuggestions" prop
|
||||
/* eslint-disable no-console */
|
||||
const originalError = console.error;
|
||||
beforeAll(() => {
|
||||
console.error = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.error = originalError;
|
||||
});
|
||||
/* eslint-enable no-console */
|
||||
const fetchTags = jest.fn();
|
||||
const formHookMock = getFormMock(sampleData);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline);
|
||||
usePostCaseMock.mockImplementation(() => defaultPostCase);
|
||||
useFormMock.mockImplementation(() => ({ form: formHookMock }));
|
||||
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
|
||||
(useGetTags as jest.Mock).mockImplementation(() => ({
|
||||
tags: sampleTags,
|
||||
fetchTags,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should post case on submit click', async () => {
|
||||
|
@ -118,4 +144,19 @@ describe('Create case', () => {
|
|||
);
|
||||
expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy();
|
||||
});
|
||||
it('Tag options render with new tags added', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="caseTags"] [data-test-subj="input"]`)
|
||||
.first()
|
||||
.prop('options')
|
||||
).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
|
@ -15,8 +15,16 @@ import {
|
|||
import styled, { css } from 'styled-components';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import { CasePostRequest } from '../../../../../../../../plugins/case/common/api';
|
||||
import { Field, Form, getUseField, useForm, UseField } from '../../../../shared_imports';
|
||||
import {
|
||||
Field,
|
||||
Form,
|
||||
getUseField,
|
||||
useForm,
|
||||
UseField,
|
||||
FormDataProvider,
|
||||
} from '../../../../shared_imports';
|
||||
import { usePostCase } from '../../../../containers/case/use_post_case';
|
||||
import { schema } from './schema';
|
||||
import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover';
|
||||
|
@ -24,6 +32,7 @@ import { useInsertTimeline } from '../../../../components/timeline/insert_timeli
|
|||
import * as i18n from '../../translations';
|
||||
import { SiemPageName } from '../../../home/types';
|
||||
import { MarkdownEditorForm } from '../../../../components/markdown_editor/form';
|
||||
import { useGetTags } from '../../../../containers/case/use_get_tags';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -59,6 +68,21 @@ export const Create = React.memo(() => {
|
|||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
const { tags: tagOptions } = useGetTags();
|
||||
const [options, setOptions] = useState(
|
||||
tagOptions.map(label => ({
|
||||
label,
|
||||
}))
|
||||
);
|
||||
useEffect(
|
||||
() =>
|
||||
setOptions(
|
||||
tagOptions.map(label => ({
|
||||
label,
|
||||
}))
|
||||
),
|
||||
[tagOptions]
|
||||
);
|
||||
const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CasePostRequest>(
|
||||
form,
|
||||
'description'
|
||||
|
@ -108,6 +132,8 @@ export const Create = React.memo(() => {
|
|||
fullWidth: true,
|
||||
placeholder: '',
|
||||
disabled: isLoading,
|
||||
options,
|
||||
noSuggestions: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -131,6 +157,25 @@ export const Create = React.memo(() => {
|
|||
}}
|
||||
/>
|
||||
</ContainerBig>
|
||||
<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>
|
||||
</Form>
|
||||
<Container>
|
||||
<EuiFlexGroup
|
||||
|
|
|
@ -6,17 +6,26 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { TagList } from './';
|
||||
import { getFormMock } from '../__mock__/form';
|
||||
import { TestProviders } from '../../../../mock';
|
||||
import { wait } from '../../../../lib/helpers';
|
||||
import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { useGetTags } from '../../../../containers/case/use_get_tags';
|
||||
|
||||
jest.mock(
|
||||
'../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
|
||||
);
|
||||
jest.mock('../../../../containers/case/use_get_tags');
|
||||
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'] }),
|
||||
})
|
||||
);
|
||||
const onSubmit = jest.fn();
|
||||
const defaultProps = {
|
||||
disabled: false,
|
||||
|
@ -26,11 +35,27 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
describe('TagList ', () => {
|
||||
// Suppress warnings about "noSuggestions" prop
|
||||
/* eslint-disable no-console */
|
||||
const originalError = console.error;
|
||||
beforeAll(() => {
|
||||
console.error = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.error = originalError;
|
||||
});
|
||||
/* eslint-enable no-console */
|
||||
const sampleTags = ['coke', 'pepsi'];
|
||||
const fetchTags = jest.fn();
|
||||
const formHookMock = getFormMock({ tags: sampleTags });
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
(useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock }));
|
||||
|
||||
(useGetTags as jest.Mock).mockImplementation(() => ({
|
||||
tags: sampleTags,
|
||||
fetchTags,
|
||||
}));
|
||||
});
|
||||
it('Renders no tags, and then edit', () => {
|
||||
const wrapper = mount(
|
||||
|
@ -80,6 +105,23 @@ describe('TagList ', () => {
|
|||
expect(onSubmit).toBeCalledWith(sampleTags);
|
||||
});
|
||||
});
|
||||
it('Tag options render with new tags added', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<TagList {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper
|
||||
.find(`[data-test-subj="tag-list-edit-button"]`)
|
||||
.last()
|
||||
.simulate('click');
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="caseTags"] [data-test-subj="input"]`)
|
||||
.first()
|
||||
.prop('options')
|
||||
).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]);
|
||||
});
|
||||
it('Cancels on cancel', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiText,
|
||||
EuiHorizontalRule,
|
||||
|
@ -17,10 +17,12 @@ import {
|
|||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import * as i18n from './translations';
|
||||
import { Form, useForm } from '../../../../shared_imports';
|
||||
import { Form, FormDataProvider, useForm } from '../../../../shared_imports';
|
||||
import { schema } from './schema';
|
||||
import { CommonUseField } from '../create';
|
||||
import { useGetTags } from '../../../../containers/case/use_get_tags';
|
||||
|
||||
interface TagListProps {
|
||||
disabled?: boolean;
|
||||
|
@ -54,6 +56,22 @@ export const TagList = React.memo(
|
|||
setIsEditTags(false);
|
||||
}
|
||||
}, [form, onSubmit]);
|
||||
const { tags: tagOptions } = useGetTags();
|
||||
const [options, setOptions] = useState(
|
||||
tagOptions.map(label => ({
|
||||
label,
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
setOptions(
|
||||
tagOptions.map(label => ({
|
||||
label,
|
||||
}))
|
||||
),
|
||||
[tagOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiText>
|
||||
|
@ -75,7 +93,7 @@ export const TagList = React.memo(
|
|||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<MyFlexGroup gutterSize="xs" data-test-subj="grr">
|
||||
<MyFlexGroup gutterSize="xs" wrap>
|
||||
{tags.length === 0 && !isEditTags && <p data-test-subj="no-tags">{i18n.NO_TAGS}</p>}
|
||||
{tags.length > 0 &&
|
||||
!isEditTags &&
|
||||
|
@ -98,9 +116,30 @@ export const TagList = React.memo(
|
|||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
placeholder: '',
|
||||
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>
|
||||
</Form>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -8,17 +8,13 @@ import React from 'react';
|
|||
import { mount } from 'enzyme';
|
||||
|
||||
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
|
||||
import { getFormMock } from '../__mock__/form';
|
||||
import { getFormMock, useFormMock } from '../__mock__/form';
|
||||
import { useUpdateComment } from '../../../../containers/case/use_update_comment';
|
||||
import { basicCase, getUserAction } from '../../../../containers/case/mock';
|
||||
import { UserActionTree } from './';
|
||||
import { TestProviders } from '../../../../mock';
|
||||
import { useFormMock } from '../create/index.test';
|
||||
import { wait } from '../../../../lib/helpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
jest.mock(
|
||||
'../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
|
||||
);
|
||||
|
||||
const fetchUserActions = jest.fn();
|
||||
const onUpdateField = jest.fn();
|
||||
|
|
|
@ -8,6 +8,7 @@ export {
|
|||
getUseField,
|
||||
getFieldValidityAndErrorMessage,
|
||||
FieldHook,
|
||||
FieldValidateResponse,
|
||||
FIELD_TYPES,
|
||||
Form,
|
||||
FormData,
|
||||
|
@ -17,6 +18,7 @@ export {
|
|||
UseField,
|
||||
useForm,
|
||||
ValidationFunc,
|
||||
VALIDATION_TYPES,
|
||||
} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
export {
|
||||
Field,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue