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,