diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts index 9d2ac29bc47d7d..cc01edcfaab112 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts @@ -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; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx index d480744fc932a7..0897be6310fa2e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx @@ -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( + + + + + + ); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 53b792bb9b5ebb..0f819f961b3963 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -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( 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(() => { }} /> + + {({ 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: ({ 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( + + + + ); + 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, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx index 9bac000b93235d..c96ae09706426c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -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 ( @@ -75,7 +93,7 @@ export const TagList = React.memo( )} - + {tags.length === 0 && !isEditTags &&

{i18n.NO_TAGS}

} {tags.length > 0 && !isEditTags && @@ -98,9 +116,30 @@ export const TagList = React.memo( euiFieldProps: { fullWidth: true, placeholder: '', + options, + noSuggestions: false, }, }} /> + + {({ 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; + }} + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx index 1c71260422d4b1..ff402e8ea1c8b4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx @@ -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(); diff --git a/x-pack/legacy/plugins/siem/public/shared_imports.ts b/x-pack/legacy/plugins/siem/public/shared_imports.ts index c83433ef129c97..0c0ac637a42293 100644 --- a/x-pack/legacy/plugins/siem/public/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/shared_imports.ts @@ -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,