diff --git a/.i18nrc.json b/.i18nrc.json index 732644b43e1f7c..85f51213c50985 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -1,5 +1,6 @@ { "paths": { + "autocomplete": "packages/kbn-securitysolution-autocomplete/src", "console": "src/plugins/console", "core": "src/core", "discover": "src/plugins/discover", diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index b656405b173d82..0b635df68aca4d 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -91,6 +91,7 @@ yarn kbn watch-bazel - @kbn/optimizer - @kbn/plugin-helpers - @kbn/rule-data-utils +- @kbn/securitysolution-autocomplete - @kbn/securitysolution-es-utils - @kbn/securitysolution-hook-utils - @kbn/securitysolution-io-ts-alerting-types diff --git a/package.json b/package.json index cddedb937f4992..70fab81d7366cc 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", "@kbn/rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils", + "@kbn/securitysolution-autocomplete": "link:bazel-bin/packages/kbn-securitysolution-autocomplete", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils", "@kbn/securitysolution-hook-utils": "link:bazel-bin/packages/kbn-securitysolution-hook-utils", "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 938afdc205a440..92144f023b7f89 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -35,6 +35,7 @@ filegroup( "//packages/kbn-plugin-generator:build", "//packages/kbn-plugin-helpers:build", "//packages/kbn-rule-data-utils:build", + "//packages/kbn-securitysolution-autocomplete:build", "//packages/kbn-securitysolution-list-constants:build", "//packages/kbn-securitysolution-io-ts-types:build", "//packages/kbn-securitysolution-io-ts-alerting-types:build", diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel new file mode 100644 index 00000000000000..8e403a215d81d8 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -0,0 +1,125 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-autocomplete" + +PKG_REQUIRE_NAME = "@kbn/securitysolution-autocomplete" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx" + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*", + "**/*.mocks.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "react/package.json", + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-dev-utils", + "//packages/kbn-i18n", + "//packages/kbn-securitysolution-io-ts-list-types", + "//packages/kbn-securitysolution-list-hooks", + "@npm//@babel/core", + "@npm//babel-loader", + "@npm//@elastic/eui", + "@npm//react", + "@npm//resize-observer-polyfill", + "@npm//rxjs", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//typescript", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + ], +) + +ts_project( + name = "tsc", + args = ["--pretty"], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = True, + declaration_dir = "target_types", + declaration_map = True, + incremental = True, + out_dir = "target_node", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = False, + incremental = True, + out_dir = "target_web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = [":tsc", ":tsc_browser"] + DEPS, +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/README.md b/packages/kbn-securitysolution-autocomplete/README.md similarity index 100% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/README.md rename to packages/kbn-securitysolution-autocomplete/README.md diff --git a/packages/kbn-securitysolution-autocomplete/babel.config.js b/packages/kbn-securitysolution-autocomplete/babel.config.js new file mode 100644 index 00000000000000..b4a118df51af51 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/babel.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + env: { + web: { + presets: ['@kbn/babel-preset/webpack_preset'], + }, + node: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + ignore: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/packages/kbn-securitysolution-autocomplete/jest.config.js b/packages/kbn-securitysolution-autocomplete/jest.config.js new file mode 100644 index 00000000000000..9b14447c983660 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-autocomplete'], +}; diff --git a/packages/kbn-securitysolution-autocomplete/package.json b/packages/kbn-securitysolution-autocomplete/package.json new file mode 100644 index 00000000000000..5cfd18b63256a9 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/securitysolution-autocomplete", + "version": "1.0.0", + "description": "Security Solution auto complete", + "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-autocomplete/react/package.json b/packages/kbn-securitysolution-autocomplete/react/package.json new file mode 100644 index 00000000000000..c5f222b5843acf --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/react/package.json @@ -0,0 +1,5 @@ +{ + "browser": "../target_web/react", + "main": "../target_node/react", + "types": "../target_types/react/index.d.ts" +} diff --git a/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts new file mode 100644 index 00000000000000..444a033b4887bb --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Copied from "src/plugins/data/public/mocks.ts" but without any type information +// TODO: Remove this in favor of the data/public/mocks if/when they become available, https://github.com/elastic/kibana/issues/100715 +export const autocompleteStartMock = { + getQuerySuggestions: jest.fn(), + getValueSuggestions: jest.fn(), + hasQuerySuggestions: jest.fn(), +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts new file mode 100644 index 00000000000000..c36184e5c5ba11 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { checkEmptyValue } from '.'; +import { getField } from '../fields/index.mock'; +import * as i18n from '../translations'; + +describe('check_empty_value', () => { + test('returns no errors if no field has been selected', () => { + const isValid = checkEmptyValue('', undefined, true, false); + + expect(isValid).toBeUndefined(); + }); + + test('returns error string if user has touched a required input and left empty', () => { + const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true); + + expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); + }); + + test('returns no errors if required input is empty but user has not yet touched it', () => { + const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if user has touched an input that is not required and left empty', () => { + const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if user has touched an input that is not required and left empty string', () => { + const isValid = checkEmptyValue('', getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns null if input value is not empty string or undefined', () => { + const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true); + + expect(isValid).toBeNull(); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts new file mode 100644 index 00000000000000..894f233f73a5a1 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as i18n from '../translations'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +/** + * Determines if empty value is ok + */ +export const checkEmptyValue = ( + param: string | undefined, + field: IFieldType | undefined, + isRequired: boolean, + touched: boolean +): string | undefined | null => { + if (isRequired && touched && (param == null || param.trim() === '')) { + return i18n.FIELD_REQUIRED_ERR; + } + + if ( + field == null || + (isRequired && !touched) || + (!isRequired && (param == null || param === '')) + ) { + return undefined; + } + + return null; +}; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx similarity index 92% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx index 416852b469a79c..08f55cef89b660 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx @@ -1,22 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { FieldComponent } from '.'; +import { fields, getField } from '../fields/index.mock'; -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -import { FieldComponent } from './field'; - -describe('FieldComponent', () => { +describe('field', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( = ({ fieldInputWidth, fieldTypeFilter = [], diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx similarity index 70% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx index b6300581f12dd8..c4c07aff909e42 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx @@ -1,14 +1,15 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { mount } from 'enzyme'; -import { AutocompleteFieldExistsComponent } from './field_value_exists'; +import { AutocompleteFieldExistsComponent } from '.'; describe('AutocompleteFieldExistsComponent', () => { test('it renders field disabled', () => { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx similarity index 83% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx index ff70204e534838..37a16406e65a33 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx similarity index 88% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx index a338ce6a27d6c4..6fcf8ddf74b031 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -11,15 +12,20 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { waitFor } from '@testing-library/react'; import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; -import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; -import { DATE_NOW, IMMUTABLE, VERSION } from '../../../../../lists/common/constants.mock'; - -import { AutocompleteFieldListsComponent } from './field_value_lists'; - -const mockKibanaHttpService = coreMock.createStart().http; +import { getField } from '../fields/index.mock'; +import { AutocompleteFieldListsComponent } from '.'; +import { + getListResponseMock, + getFoundListSchemaMock, + DATE_NOW, + IMMUTABLE, + VERSION, +} from '../list_schema/index.mock'; + +// TODO: Once these mocks are available, use them instead of hand mocking, https://github.com/elastic/kibana/issues/100715 +// const mockKibanaHttpService = coreMock.createStart().http; +// import { coreMock } from '../../../../../../../src/core/public/mocks'; +const mockKibanaHttpService = jest.fn(); const mockStart = jest.fn(); const mockKeywordList: ListSchema = { @@ -35,7 +41,6 @@ jest.mock('@kbn/securitysolution-list-hooks', () => { return { ...originalModule, - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type useFindLists: () => ({ error: undefined, loading: false, diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx similarity index 80% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx index 047f8ef33c8c0c..4064ff11962bde 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx @@ -1,20 +1,28 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; -import { HttpStart } from 'kibana/public'; import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; import { useFindLists } from '@kbn/securitysolution-list-hooks'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { filterFieldToList } from '../filter_field_to_list'; +import { getGenericComboBoxProps } from '../get_generic_combo_box_props'; -import { filterFieldToList, getGenericComboBoxProps } from './helpers'; -import * as i18n from './translations'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { HttpStart } from 'kibana/public'; +type HttpStart = any; + +import * as i18n from '../translations'; const SINGLE_SELECTION = { asPlainText: true }; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx similarity index 95% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx index c1ffb008e85635..d695088245622f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx @@ -1,27 +1,21 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption, EuiSuperSelect } from '@elastic/eui'; import { act } from '@testing-library/react'; +import { AutocompleteFieldMatchComponent } from '.'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { fields, getField } from '../fields/index.mock'; +import { autocompleteStartMock } from '../autocomplete/index.mock'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -import { AutocompleteFieldMatchComponent } from './field_value_match'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -jest.mock('./hooks/use_field_value_autocomplete'); - -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); +jest.mock('../hooks/use_field_value_autocomplete'); describe('AutocompleteFieldMatchComponent', () => { let wrapper: ReactWrapper; @@ -299,7 +293,6 @@ describe('AutocompleteFieldMatchComponent', () => { selectedValue="" /> ); - expect( wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').exists() ).toBeTruthy(); @@ -431,7 +424,6 @@ describe('AutocompleteFieldMatchComponent', () => { selectedValue="" /> ); - wrapper .find('[data-test-subj="valueAutocompleteFieldMatchNumber"] input') .at(0) diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx similarity index 85% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx index 8dbe8f223ae5bd..8199967489515f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx @@ -1,28 +1,39 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { - EuiComboBox, - EuiComboBoxOptionOption, - EuiFieldNumber, - EuiFormRow, EuiSuperSelect, + EuiFormRow, + EuiFieldNumber, + EuiComboBoxOptionOption, + EuiComboBox, } from '@elastic/eui'; import { uniq } from 'lodash'; + import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +type AutocompleteStart = any; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; -import { getGenericComboBoxProps, paramIsValid } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; + +import * as i18n from '../translations'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { + getGenericComboBoxProps, + GetGenericComboBoxPropsReturn, +} from '../get_generic_combo_box_props'; +import { paramIsValid } from '../param_is_valid'; const BOOLEAN_OPTIONS = [ { inputDisplay: 'true', value: 'true' }, @@ -47,11 +58,6 @@ interface AutocompleteFieldMatchProps { onError?: (arg: boolean) => void; } -/** - * There is a copy of this within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ export const AutocompleteFieldMatchComponent: React.FC = ({ placeholder, rowLabel, @@ -189,11 +195,6 @@ export const AutocompleteFieldMatchComponent: React.FC (fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}), - [fieldInputWidth] - ); - useEffect((): void => { setError(undefined); if (onError != null) { @@ -225,7 +226,7 @@ export const AutocompleteFieldMatchComponent: React.FC @@ -234,7 +235,7 @@ export const AutocompleteFieldMatchComponent: React.FC @@ -289,7 +290,7 @@ export const AutocompleteFieldMatchComponent: React.FC diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx similarity index 91% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx index 8aa1f18b695a0e..a3ca97874908e1 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -10,18 +11,18 @@ import { ReactWrapper, mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { act } from '@testing-library/react'; -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { AutocompleteFieldMatchAnyComponent } from '.'; +import { getField, fields } from '../fields/index.mock'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { autocompleteStartMock } from '../autocomplete/index.mock'; -import { AutocompleteFieldMatchAnyComponent } from './field_value_match_any'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); - -jest.mock('./hooks/use_field_value_autocomplete'); +jest.mock('../hooks/use_field_value_autocomplete', () => { + const actual = jest.requireActual('../hooks/use_field_value_autocomplete'); + return { + ...actual, + useFieldValueAutocomplete: jest.fn(), + }; +}); describe('AutocompleteFieldMatchAnyComponent', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx similarity index 86% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx index e5a5e76f8cc5d6..338c4baa8bc6fc 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useCallback, useMemo, useState } from 'react'; @@ -10,13 +11,22 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { uniq } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; - -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; -import { getGenericComboBoxProps, paramIsValid } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +type AutocompleteStart = any; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; + +import * as i18n from '../translations'; +import { + getGenericComboBoxProps, + GetGenericComboBoxPropsReturn, +} from '../get_generic_combo_box_props'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { paramIsValid } from '../param_is_valid'; interface AutocompleteFieldMatchAnyProps { placeholder: string; diff --git a/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts new file mode 100644 index 00000000000000..5938ed34547a1f --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts @@ -0,0 +1,313 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Copied from "src/plugins/data/common/index_patterns/fields/fields.mocks.ts" +// but without types. +// TODO: This should move out once those mocks are directly useable or in their own package, https://github.com/elastic/kibana/issues/100715 + +export const fields = [ + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'ssl', + type: 'boolean', + esTypes: ['boolean'], + count: 20, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + count: 30, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'time', + type: 'date', + esTypes: ['date'], + count: 30, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '@tags', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'utc_time', + type: 'date', + esTypes: ['date'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'phpmemory', + type: 'number', + esTypes: ['integer'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'ip', + type: 'ip', + esTypes: ['ip'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'request_body', + type: 'attachment', + esTypes: ['attachment'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'point', + type: 'geo_point', + esTypes: ['geo_point'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'area', + type: 'geo_shape', + esTypes: ['geo_shape'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'hashed', + type: 'murmur3', + esTypes: ['murmur3'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'geo.coordinates', + type: 'geo_point', + esTypes: ['geo_point'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'extension', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'machine.os', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'machine.os.raw', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { multi: { parent: 'machine.os' } }, + }, + { + name: 'geo.src', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '_id', + type: 'string', + esTypes: ['_id'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: '_type', + type: 'string', + esTypes: ['_type'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: '_source', + type: '_source', + esTypes: ['_source'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'non-filterable', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'non-sortable', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'custom_user_field', + type: 'conflict', + esTypes: ['long', 'text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'script string', + type: 'string', + count: 0, + scripted: true, + script: "'i am a string'", + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script number', + type: 'number', + count: 0, + scripted: true, + script: '1234', + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script date', + type: 'date', + count: 0, + scripted: true, + script: '1234', + lang: 'painless', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script murmur3', + type: 'murmur3', + count: 0, + scripted: true, + script: '1234', + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'nestedField.child', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'nestedField' } }, + }, + { + name: 'nestedField.nestedChild.doublyNestedChild', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'nestedField.nestedChild' } }, + }, +]; + +export const getField = (name: string) => fields.find((field) => field.name === name); diff --git a/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts new file mode 100644 index 00000000000000..1022849ffda360 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { filterFieldToList } from '.'; + +import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { getListResponseMock } from '../list_schema/index.mock'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +describe('#filterFieldToList', () => { + test('it returns empty array if given a undefined for field', () => { + const filter = filterFieldToList([], undefined); + expect(filter).toEqual([]); + }); + + test('it returns empty array if filed does not contain esTypes', () => { + const field: IFieldType = { name: 'some-name', type: 'some-type' }; + const filter = filterFieldToList([], field); + expect(filter).toEqual([]); + }); + + test('it returns single filtered list of ip_range -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of ip -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'ip' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of keyword -> keyword', () => { + const field: IFieldType = { esTypes: ['keyword'], name: 'some-name', type: 'keyword' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'keyword' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of text -> text', () => { + const field: IFieldType = { esTypes: ['text'], name: 'some-name', type: 'text' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'text' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns 2 filtered lists of ip_range -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const listItem2: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const filter = filterFieldToList([listItem1, listItem2], field); + const expected: ListSchema[] = [listItem1, listItem2]; + expect(filter).toEqual(expected); + }); + + test('it returns 1 filtered lists of ip_range -> ip if the 2nd is not compatible type', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const listItem2: ListSchema = { ...getListResponseMock(), type: 'text' }; + const filter = filterFieldToList([listItem1, listItem2], field); + const expected: ListSchema[] = [listItem1]; + expect(filter).toEqual(expected); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts new file mode 100644 index 00000000000000..b2e48c25f9b51f --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { typeMatch } from '../type_match'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +/** + * Given an array of lists and optionally a field this will return all + * the lists that match against the field based on the types from the field + * @param lists The lists to match against the field + * @param field The field to check against the list to see if they are compatible + */ +export const filterFieldToList = (lists: ListSchema[], field?: IFieldType): ListSchema[] => { + if (field != null) { + const { esTypes = [] } = field; + return lists.filter(({ type }) => esTypes.some((esType: string) => typeMatch(type, esType))); + } else { + return []; + } +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx new file mode 100644 index 00000000000000..63a94be1271a7c --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getGenericComboBoxProps } from '.'; + +describe('get_generic_combo_box_props', () => { + test('it returns empty arrays if "options" is empty array', () => { + const result = getGenericComboBoxProps({ + options: [], + selectedOptions: ['option1'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); + }); + + test('it returns formatted props if "options" array is not empty', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: [], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [], + }); + }); + + test('it does not return "selectedOptions" items that do not appear in "options"', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: ['option4'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [], + }); + }); + + test('it return "selectedOptions" items that do appear in "options"', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: ['option2'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [ + { + label: 'option2', + }, + ], + }); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts new file mode 100644 index 00000000000000..0fba3c39344b80 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiComboBoxOptionOption } from '@elastic/eui'; + +export interface GetGenericComboBoxPropsReturn { + comboOptions: EuiComboBoxOptionOption[]; + labels: string[]; + selectedComboOptions: EuiComboBoxOptionOption[]; +} + +/** + * Determines the options, selected values and option labels for EUI combo box + * @param options options user can select from + * @param selectedOptions user selection if any + * @param getLabel helper function to know which property to use for labels + */ +export const getGenericComboBoxProps = ({ + getLabel, + options, + selectedOptions, +}: { + getLabel: (value: T) => string; + options: T[]; + selectedOptions: T[]; +}): GetGenericComboBoxPropsReturn => { + const newLabels = options.map(getLabel); + const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); + const newSelectedComboOptions = selectedOptions + .map(getLabel) + .filter((option) => { + return newLabels.indexOf(option) !== -1; + }) + .map((option) => { + return newComboOptions[newLabels.indexOf(option)]; + }); + + return { + comboOptions: newComboOptions, + labels: newLabels, + selectedComboOptions: newSelectedComboOptions, + }; +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts new file mode 100644 index 00000000000000..e473df104fa6a4 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + doesNotExistOperator, + EXCEPTION_OPERATORS, + existsOperator, + isNotOperator, + isOperator, +} from '@kbn/securitysolution-list-utils'; +import { getOperators } from '.'; +import { getField } from '../fields/index.mock'; + +describe('#getOperators', () => { + test('it returns "isOperator" if passed in field is "undefined"', () => { + const operator = getOperators(undefined); + + expect(operator).toEqual([isOperator]); + }); + + test('it returns expected operators when field type is "boolean"', () => { + const operator = getOperators(getField('ssl')); + + expect(operator).toEqual([isOperator, isNotOperator, existsOperator, doesNotExistOperator]); + }); + + test('it returns "isOperator" when field type is "nested"', () => { + const operator = getOperators({ + aggregatable: false, + count: 0, + esTypes: ['text'], + name: 'nestedField', + readFromDocValues: false, + scripted: false, + searchable: true, + subType: { nested: { path: 'nestedField' } }, + type: 'nested', + }); + + expect(operator).toEqual([isOperator]); + }); + + test('it returns all operator types when field type is not null, boolean, or nested', () => { + const operator = getOperators(getField('machine.os.raw')); + + expect(operator).toEqual(EXCEPTION_OPERATORS); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts new file mode 100644 index 00000000000000..39d2779e2dc44f --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +import { + EXCEPTION_OPERATORS, + OperatorOption, + doesNotExistOperator, + existsOperator, + isNotOperator, + isOperator, +} from '@kbn/securitysolution-list-utils'; + +/** + * Returns the appropriate operators given a field type + * + * @param field IFieldType selected field + * + */ +export const getOperators = (field: IFieldType | undefined): OperatorOption[] => { + if (field == null) { + return [isOperator]; + } else if (field.type === 'boolean') { + return [isOperator, isNotOperator, existsOperator, doesNotExistOperator]; + } else if (field.type === 'nested') { + return [isOperator]; + } else { + return EXCEPTION_OPERATORS; + } +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts new file mode 100644 index 00000000000000..cc5a37bfc46f07 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export * from './use_field_value_autocomplete'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts similarity index 92% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts rename to packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts index 0335ffa55d2a28..534daa021cf4a8 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts @@ -1,28 +1,40 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { act, renderHook } from '@testing-library/react-hooks'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; -import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; - import { UseFieldValueAutocompleteProps, UseFieldValueAutocompleteReturn, useFieldValueAutocomplete, -} from './use_field_value_autocomplete'; - -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); - -jest.mock('../../../../../../../../src/plugins/kibana_react/public'); - -describe('useFieldValueAutocomplete', () => { +} from '.'; +import { getField } from '../../fields/index.mock'; +import { autocompleteStartMock } from '../../autocomplete/index.mock'; + +// Copied from "src/plugins/data/common/index_patterns/index_pattern.stub.ts" +// TODO: Remove this in favor of the above if/when it is ported, https://github.com/elastic/kibana/issues/100715 +export const stubIndexPatternWithFields = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], +}; + +describe('use_field_value_autocomplete', () => { const onErrorMock = jest.fn(); const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts similarity index 81% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts rename to packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts index 63d3925d6d64d3..b4dec1615e3ed4 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts @@ -1,16 +1,23 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { useEffect, useRef, useState } from 'react'; import { debounce } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public'; -import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public'; +type AutocompleteStart = any; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; interface FuncArgs { fieldSelected: IFieldType | undefined; @@ -33,10 +40,6 @@ export interface UseFieldValueAutocompleteProps { } /** * Hook for using the field value autocomplete service - * There is a copy within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 */ export const useFieldValueAutocomplete = ({ selectedField, diff --git a/packages/kbn-securitysolution-autocomplete/src/index.ts b/packages/kbn-securitysolution-autocomplete/src/index.ts new file mode 100644 index 00000000000000..5fcb3f954189ad --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export * from './check_empty_value'; +export * from './field'; +export * from './field_value_exists'; +export * from './field_value_lists'; +export * from './field_value_match'; +export * from './field_value_match_any'; +export * from './filter_field_to_list'; +export * from './get_generic_combo_box_props'; +export * from './get_operators'; +export * from './hooks'; +export * from './operator'; +export * from './param_is_valid'; diff --git a/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts new file mode 100644 index 00000000000000..fb629ad2f946ed --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FoundListSchema, ListSchema } from '@kbn/securitysolution-io-ts-list-types'; + +// TODO: Once this mock is available within packages, use it instead, https://github.com/elastic/kibana/issues/100715 +// import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; +export const getFoundListSchemaMock = (): FoundListSchema => ({ + cursor: '123', + data: [getListResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); + +// TODO: Once these mocks are available from packages use it instead, https://github.com/elastic/kibana/issues/100715 +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; +export const USER = 'some user'; +export const IMMUTABLE = false; +export const VERSION = 1; +export const DESCRIPTION = 'some description'; +export const TIE_BREAKER = '6a76b69d-80df-4ab2-8c3e-85f466b06a0e'; +export const LIST_ID = 'some-list-id'; +export const META = {}; +export const TYPE = 'ip'; +export const NAME = 'some name'; + +// TODO: Once this mock is available within packages, use it instead, https://github.com/elastic/kibana/issues/100715 +// import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; +export const getListResponseMock = (): ListSchema => ({ + _version: undefined, + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + deserializer: undefined, + id: LIST_ID, + immutable: IMMUTABLE, + meta: META, + name: NAME, + serializer: undefined, + tie_breaker_id: TIE_BREAKER, + type: TYPE, + updated_at: DATE_NOW, + updated_by: USER, + version: VERSION, +}); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx b/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx similarity index 95% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx index dadde8800b67f6..fed7007b496361 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -10,11 +11,10 @@ import { mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { isNotOperator, isOperator } from '@kbn/securitysolution-list-utils'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; +import { OperatorComponent } from '.'; +import { getField } from '../fields/index.mock'; -import { OperatorComponent } from './operator'; - -describe('OperatorComponent', () => { +describe('operator', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( { + beforeEach(() => { + // Disable momentJS deprecation warning and it looks like it is not typed either so + // we have to disable the type as well and cannot extend it easily. + ((moment as unknown) as { + suppressDeprecationWarnings: boolean; + }).suppressDeprecationWarnings = true; + }); + + afterEach(() => { + // Re-enable momentJS deprecation warning and it looks like it is not typed either so + // we have to disable the type as well and cannot extend it easily. + ((moment as unknown) as { + suppressDeprecationWarnings: boolean; + }).suppressDeprecationWarnings = false; + }); + + test('returns no errors if no field has been selected', () => { + const isValid = paramIsValid('', undefined, true, false); + + expect(isValid).toBeUndefined(); + }); + + test('returns error string if user has touched a required input and left empty', () => { + const isValid = paramIsValid(undefined, getField('@timestamp'), true, true); + + expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); + }); + + test('returns no errors if required input is empty but user has not yet touched it', () => { + const isValid = paramIsValid(undefined, getField('@timestamp'), true, false); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if user has touched an input that is not required and left empty', () => { + const isValid = paramIsValid(undefined, getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if user has touched an input that is not required and left empty string', () => { + const isValid = paramIsValid('', getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if field is of type date and value is valid', () => { + const isValid = paramIsValid('1994-11-05T08:15:30-05:00', getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns errors if filed is of type date and value is not valid', () => { + const isValid = paramIsValid('1593478826', getField('@timestamp'), false, true); + + expect(isValid).toEqual(i18n.DATE_ERR); + }); + + test('returns no errors if field is of type number and value is an integer', () => { + const isValid = paramIsValid('4', getField('bytes'), true, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if field is of type number and value is a float', () => { + const isValid = paramIsValid('4.3', getField('bytes'), true, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if field is of type number and value is a long', () => { + const isValid = paramIsValid('-9223372036854775808', getField('bytes'), true, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns errors if field is of type number and value is "hello"', () => { + const isValid = paramIsValid('hello', getField('bytes'), true, true); + + expect(isValid).toEqual(i18n.NUMBER_ERR); + }); + + test('returns errors if field is of type number and value is "123abc"', () => { + const isValid = paramIsValid('123abc', getField('bytes'), true, true); + + expect(isValid).toEqual(i18n.NUMBER_ERR); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts new file mode 100644 index 00000000000000..5b596b4b624086 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import dateMath from '@elastic/datemath'; +import { checkEmptyValue } from '../check_empty_value'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +import * as i18n from '../translations'; + +/** + * Very basic validation for values + * @param param the value being checked + * @param field the selected field + * @param isRequired whether or not an empty value is allowed + * @param touched has field been touched by user + * @returns undefined if valid, string with error message if invalid + */ +export const paramIsValid = ( + param: string | undefined, + field: IFieldType | undefined, + isRequired: boolean, + touched: boolean +): string | undefined => { + if (field == null) { + return undefined; + } + + const emptyValueError = checkEmptyValue(param, field, isRequired, touched); + if (emptyValueError !== null) { + return emptyValueError; + } + + switch (field.type) { + case 'date': + const moment = dateMath.parse(param ?? ''); + const isDate = Boolean(moment && moment.isValid()); + return isDate ? undefined : i18n.DATE_ERR; + case 'number': + const isNum = param != null && param.trim() !== '' && !isNaN(+param); + return isNum ? undefined : i18n.NUMBER_ERR; + default: + return undefined; + } +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/translations/index.ts b/packages/kbn-securitysolution-autocomplete/src/translations/index.ts new file mode 100644 index 00000000000000..35d6531be51bdc --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/translations/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOADING = i18n.translate('autocomplete.loadingDescription', { + defaultMessage: 'Loading...', +}); + +export const SELECT_FIELD_FIRST = i18n.translate('autocomplete.selectField', { + defaultMessage: 'Please select a field first...', +}); + +export const FIELD_REQUIRED_ERR = i18n.translate('autocomplete.fieldRequiredError', { + defaultMessage: 'Value cannot be empty', +}); + +export const NUMBER_ERR = i18n.translate('autocomplete.invalidNumberError', { + defaultMessage: 'Not a valid number', +}); + +export const DATE_ERR = i18n.translate('autocomplete.invalidDateError', { + defaultMessage: 'Not a valid date', +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts new file mode 100644 index 00000000000000..4694313720c792 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { typeMatch } from '.'; + +describe('type_match', () => { + test('ip -> ip is true', () => { + expect(typeMatch('ip', 'ip')).toEqual(true); + }); + + test('keyword -> keyword is true', () => { + expect(typeMatch('keyword', 'keyword')).toEqual(true); + }); + + test('text -> text is true', () => { + expect(typeMatch('text', 'text')).toEqual(true); + }); + + test('ip_range -> ip is true', () => { + expect(typeMatch('ip_range', 'ip')).toEqual(true); + }); + + test('date_range -> date is true', () => { + expect(typeMatch('date_range', 'date')).toEqual(true); + }); + + test('double_range -> double is true', () => { + expect(typeMatch('double_range', 'double')).toEqual(true); + }); + + test('float_range -> float is true', () => { + expect(typeMatch('float_range', 'float')).toEqual(true); + }); + + test('integer_range -> integer is true', () => { + expect(typeMatch('integer_range', 'integer')).toEqual(true); + }); + + test('long_range -> long is true', () => { + expect(typeMatch('long_range', 'long')).toEqual(true); + }); + + test('ip -> date is false', () => { + expect(typeMatch('ip', 'date')).toEqual(false); + }); + + test('long -> float is false', () => { + expect(typeMatch('long', 'float')).toEqual(false); + }); + + test('integer -> long is false', () => { + expect(typeMatch('integer', 'long')).toEqual(false); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts b/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts new file mode 100644 index 00000000000000..d5476f3b32b49b --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; + +/** + * Given an input list type and a string based ES type this will match + * if they're exact or if they are compatible with a range + * @param type The type to match against the esType + * @param esType The ES type to match with + */ +export const typeMatch = (type: Type, esType: string): boolean => { + return ( + type === esType || + (type === 'ip_range' && esType === 'ip') || + (type === 'date_range' && esType === 'date') || + (type === 'double_range' && esType === 'double') || + (type === 'float_range' && esType === 'float') || + (type === 'integer_range' && esType === 'integer') || + (type === 'long_range' && esType === 'long') + ); +}; diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json new file mode 100644 index 00000000000000..bab7b18c59cfde --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "outDir": "./target_web", + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-securitysolution-autocomplete/src", + "types": [ + "jest", + "node" + ], + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.json b/packages/kbn-securitysolution-autocomplete/tsconfig.json new file mode 100644 index 00000000000000..bf402e93ffd698 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "declarationDir": "./target_types", + "outDir": "target_node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-autocomplete/src", + "rootDir": "src", + "types": ["jest", "node", "resize-observer-polyfill"] + }, + "include": ["src/**/*"] +} diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts deleted file mode 100644 index 21764c6f459d87..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts +++ /dev/null @@ -1,388 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { - EXCEPTION_OPERATORS, - doesNotExistOperator, - existsOperator, - isNotOperator, - isOperator, -} from '@kbn/securitysolution-list-utils'; - -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; - -import * as i18n from './translations'; -import { - checkEmptyValue, - filterFieldToList, - getGenericComboBoxProps, - getOperators, - paramIsValid, - typeMatch, -} from './helpers'; - -describe('helpers', () => { - // @ts-ignore - moment.suppressDeprecationWarnings = true; - describe('#getOperators', () => { - test('it returns "isOperator" if passed in field is "undefined"', () => { - const operator = getOperators(undefined); - - expect(operator).toEqual([isOperator]); - }); - - test('it returns expected operators when field type is "boolean"', () => { - const operator = getOperators(getField('ssl')); - - expect(operator).toEqual([isOperator, isNotOperator, existsOperator, doesNotExistOperator]); - }); - - test('it returns "isOperator" when field type is "nested"', () => { - const operator = getOperators({ - aggregatable: false, - count: 0, - esTypes: ['text'], - name: 'nestedField', - readFromDocValues: false, - scripted: false, - searchable: true, - subType: { nested: { path: 'nestedField' } }, - type: 'nested', - }); - - expect(operator).toEqual([isOperator]); - }); - - test('it returns all operator types when field type is not null, boolean, or nested', () => { - const operator = getOperators(getField('machine.os.raw')); - - expect(operator).toEqual(EXCEPTION_OPERATORS); - }); - }); - - describe('#checkEmptyValue', () => { - test('returns no errors if no field has been selected', () => { - const isValid = checkEmptyValue('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = checkEmptyValue('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns null if input value is not empty string or undefined', () => { - const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true); - - expect(isValid).toBeNull(); - }); - }); - - describe('#paramIsValid', () => { - test('returns no errors if no field has been selected', () => { - const isValid = paramIsValid('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = paramIsValid('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type date and value is valid', () => { - const isValid = paramIsValid( - '1994-11-05T08:15:30-05:00', - getField('@timestamp'), - false, - true - ); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if filed is of type date and value is not valid', () => { - const isValid = paramIsValid('1593478826', getField('@timestamp'), false, true); - - expect(isValid).toEqual(i18n.DATE_ERR); - }); - - test('returns no errors if field is of type number and value is an integer', () => { - const isValid = paramIsValid('4', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a float', () => { - const isValid = paramIsValid('4.3', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a long', () => { - const isValid = paramIsValid('-9223372036854775808', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if field is of type number and value is "hello"', () => { - const isValid = paramIsValid('hello', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - - test('returns errors if field is of type number and value is "123abc"', () => { - const isValid = paramIsValid('123abc', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - }); - - describe('#getGenericComboBoxProps', () => { - test('it returns empty arrays if "options" is empty array', () => { - const result = getGenericComboBoxProps({ - getLabel: (t: string) => t, - options: [], - selectedOptions: ['option1'], - }); - - expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); - }); - - test('it returns formatted props if "options" array is not empty', () => { - const result = getGenericComboBoxProps({ - getLabel: (t: string) => t, - options: ['option1', 'option2', 'option3'], - selectedOptions: [], - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it does not return "selectedOptions" items that do not appear in "options"', () => { - const result = getGenericComboBoxProps({ - getLabel: (t: string) => t, - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option4'], - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it return "selectedOptions" items that do appear in "options"', () => { - const result = getGenericComboBoxProps({ - getLabel: (t: string) => t, - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option2'], - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [ - { - label: 'option2', - }, - ], - }); - }); - }); - - describe('#typeMatch', () => { - test('ip -> ip is true', () => { - expect(typeMatch('ip', 'ip')).toEqual(true); - }); - - test('keyword -> keyword is true', () => { - expect(typeMatch('keyword', 'keyword')).toEqual(true); - }); - - test('text -> text is true', () => { - expect(typeMatch('text', 'text')).toEqual(true); - }); - - test('ip_range -> ip is true', () => { - expect(typeMatch('ip_range', 'ip')).toEqual(true); - }); - - test('date_range -> date is true', () => { - expect(typeMatch('date_range', 'date')).toEqual(true); - }); - - test('double_range -> double is true', () => { - expect(typeMatch('double_range', 'double')).toEqual(true); - }); - - test('float_range -> float is true', () => { - expect(typeMatch('float_range', 'float')).toEqual(true); - }); - - test('integer_range -> integer is true', () => { - expect(typeMatch('integer_range', 'integer')).toEqual(true); - }); - - test('long_range -> long is true', () => { - expect(typeMatch('long_range', 'long')).toEqual(true); - }); - - test('ip -> date is false', () => { - expect(typeMatch('ip', 'date')).toEqual(false); - }); - - test('long -> float is false', () => { - expect(typeMatch('long', 'float')).toEqual(false); - }); - - test('integer -> long is false', () => { - expect(typeMatch('integer', 'long')).toEqual(false); - }); - }); - - describe('#filterFieldToList', () => { - test('it returns empty array if given a undefined for field', () => { - const filter = filterFieldToList([], undefined); - expect(filter).toEqual([]); - }); - - test('it returns empty array if filed does not contain esTypes', () => { - const field: IFieldType = { name: 'some-name', type: 'some-type' }; - const filter = filterFieldToList([], field); - expect(filter).toEqual([]); - }); - - test('it returns single filtered list of ip_range -> ip', () => { - const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; - const listItem: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; - const filter = filterFieldToList([listItem], field); - const expected: ListSchema[] = [listItem]; - expect(filter).toEqual(expected); - }); - - test('it returns single filtered list of ip -> ip', () => { - const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; - const listItem: ListSchema = { ...getListResponseMock(), type: 'ip' }; - const filter = filterFieldToList([listItem], field); - const expected: ListSchema[] = [listItem]; - expect(filter).toEqual(expected); - }); - - test('it returns single filtered list of keyword -> keyword', () => { - const field: IFieldType = { esTypes: ['keyword'], name: 'some-name', type: 'keyword' }; - const listItem: ListSchema = { ...getListResponseMock(), type: 'keyword' }; - const filter = filterFieldToList([listItem], field); - const expected: ListSchema[] = [listItem]; - expect(filter).toEqual(expected); - }); - - test('it returns single filtered list of text -> text', () => { - const field: IFieldType = { esTypes: ['text'], name: 'some-name', type: 'text' }; - const listItem: ListSchema = { ...getListResponseMock(), type: 'text' }; - const filter = filterFieldToList([listItem], field); - const expected: ListSchema[] = [listItem]; - expect(filter).toEqual(expected); - }); - - test('it returns 2 filtered lists of ip_range -> ip', () => { - const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; - const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; - const listItem2: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; - const filter = filterFieldToList([listItem1, listItem2], field); - const expected: ListSchema[] = [listItem1, listItem2]; - expect(filter).toEqual(expected); - }); - - test('it returns 1 filtered lists of ip_range -> ip if the 2nd is not compatible type', () => { - const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; - const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; - const listItem2: ListSchema = { ...getListResponseMock(), type: 'text' }; - const filter = filterFieldToList([listItem1, listItem2], field); - const expected: ListSchema[] = [listItem1]; - expect(filter).toEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts deleted file mode 100644 index 975416e272227c..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts +++ /dev/null @@ -1,183 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import dateMath from '@elastic/datemath'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; -import type { ListSchema, Type } from '@kbn/securitysolution-io-ts-list-types'; -import { - EXCEPTION_OPERATORS, - OperatorOption, - doesNotExistOperator, - existsOperator, - isNotOperator, - isOperator, -} from '@kbn/securitysolution-list-utils'; - -import { IFieldType } from '../../../../../../../src/plugins/data/common'; - -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - -/** - * Returns the appropriate operators given a field type - * - * @param field IFieldType selected field - * - */ -export const getOperators = (field: IFieldType | undefined): OperatorOption[] => { - if (field == null) { - return [isOperator]; - } else if (field.type === 'boolean') { - return [isOperator, isNotOperator, existsOperator, doesNotExistOperator]; - } else if (field.type === 'nested') { - return [isOperator]; - } else { - return EXCEPTION_OPERATORS; - } -}; - -/** - * Determines if empty value is ok - * - * @param param the value being checked - * @param field the selected field - * @param isRequired whether or not an empty value is allowed - * @param touched has field been touched by user - * @returns undefined if valid, string with error message if invalid, - * null if no checks matched - */ -export const checkEmptyValue = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined | null => { - if (isRequired && touched && (param == null || param.trim() === '')) { - return i18n.FIELD_REQUIRED_ERR; - } - - if ( - field == null || - (isRequired && !touched) || - (!isRequired && (param == null || param === '')) - ) { - return undefined; - } - - return null; -}; - -/** - * Very basic validation for values - * There is a copy within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * - * @param param the value being checked - * @param field the selected field - * @param isRequired whether or not an empty value is allowed - * @param touched has field been touched by user - * @returns undefined if valid, string with error message if invalid - */ -export const paramIsValid = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined => { - if (field == null) { - return undefined; - } - - const emptyValueError = checkEmptyValue(param, field, isRequired, touched); - if (emptyValueError !== null) { - return emptyValueError; - } - - switch (field.type) { - case 'date': - const moment = dateMath.parse(param ?? ''); - const isDate = Boolean(moment && moment.isValid()); - return isDate ? undefined : i18n.DATE_ERR; - case 'number': - const isNum = param != null && param.trim() !== '' && !isNaN(+param); - return isNum ? undefined : i18n.NUMBER_ERR; - default: - return undefined; - } -}; - -/** - * Determines the options, selected values and option labels for EUI combo box - * There is a copy within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * @param options options user can select from - * @param selectedOptions user selection if any - * @param getLabel helper function to know which property to use for labels - */ -export const getGenericComboBoxProps = ({ - getLabel, - options, - selectedOptions, -}: { - getLabel: (value: T) => string; - options: T[]; - selectedOptions: T[]; -}): GetGenericComboBoxPropsReturn => { - const newLabels = options.map(getLabel); - const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); - const newSelectedComboOptions = selectedOptions - .map(getLabel) - .filter((option) => { - return newLabels.indexOf(option) !== -1; - }) - .map((option) => { - return newComboOptions[newLabels.indexOf(option)]; - }); - - return { - comboOptions: newComboOptions, - labels: newLabels, - selectedComboOptions: newSelectedComboOptions, - }; -}; - -/** - * Given an array of lists and optionally a field this will return all - * the lists that match against the field based on the types from the field - * @param lists The lists to match against the field - * @param field The field to check against the list to see if they are compatible - */ -export const filterFieldToList = (lists: ListSchema[], field?: IFieldType): ListSchema[] => { - if (field != null) { - const { esTypes = [] } = field; - return lists.filter(({ type }) => esTypes.some((esType) => typeMatch(type, esType))); - } else { - return []; - } -}; - -/** - * Given an input list type and a string based ES type this will match - * if they're exact or if they are compatible with a range - * @param type The type to match against the esType - * @param esType The ES type to match with - */ -export const typeMatch = (type: Type, esType: string): boolean => { - return ( - type === esType || - (type === 'ip_range' && esType === 'ip') || - (type === 'date_range' && esType === 'date') || - (type === 'double_range' && esType === 'double') || - (type === 'float_range' && esType === 'float') || - (type === 'integer_range' && esType === 'integer') || - (type === 'long_range' && esType === 'long') - ); -}; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx deleted file mode 100644 index 1623683f25ed55..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx +++ /dev/null @@ -1,13 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { AutocompleteFieldExistsComponent } from './field_value_exists'; -export { AutocompleteFieldListsComponent } from './field_value_lists'; -export { AutocompleteFieldMatchAnyComponent } from './field_value_match_any'; -export { AutocompleteFieldMatchComponent } from './field_value_match'; -export { FieldComponent } from './field'; -export { OperatorComponent } from './operator'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts deleted file mode 100644 index 065239246d3299..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts +++ /dev/null @@ -1,28 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const LOADING = i18n.translate('xpack.lists.autocomplete.loadingDescription', { - defaultMessage: 'Loading...', -}); - -export const SELECT_FIELD_FIRST = i18n.translate('xpack.lists.autocomplete.selectField', { - defaultMessage: 'Please select a field first...', -}); - -export const FIELD_REQUIRED_ERR = i18n.translate('xpack.lists.autocomplete.fieldRequiredError', { - defaultMessage: 'Value cannot be empty', -}); - -export const NUMBER_ERR = i18n.translate('xpack.lists.autocomplete.invalidNumberError', { - defaultMessage: 'Not a valid number', -}); - -export const DATE_ERR = i18n.translate('xpack.lists.autocomplete.invalidDateError', { - defaultMessage: 'Not a valid date', -}); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts deleted file mode 100644 index 07f1903fb70e1c..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts +++ /dev/null @@ -1,14 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiComboBoxOptionOption } from '@elastic/eui'; - -export interface GetGenericComboBoxPropsReturn { - comboOptions: EuiComboBoxOptionOption[]; - labels: string[]; - selectedComboOptions: EuiComboBoxOptionOption[]; -} diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index c54da89766d76e..d7741b3fe0ff10 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -27,16 +27,18 @@ import { getFilteredIndexPatterns, getOperatorOptions, } from '@kbn/securitysolution-list-utils'; +import { + AutocompleteFieldExistsComponent, + AutocompleteFieldListsComponent, + AutocompleteFieldMatchAnyComponent, + AutocompleteFieldMatchComponent, + FieldComponent, + OperatorComponent, +} from '@kbn/securitysolution-autocomplete'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { HttpStart } from '../../../../../../../src/core/public'; -import { FieldComponent } from '../autocomplete/field'; -import { OperatorComponent } from '../autocomplete/operator'; -import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_exists'; -import { AutocompleteFieldMatchComponent } from '../autocomplete/field_value_match'; -import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any'; -import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists'; import { getEmptyValue } from '../../../common/empty_value'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx deleted file mode 100644 index 79e6fe5506b84a..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx +++ /dev/null @@ -1,146 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; - -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { FieldComponent } from './field'; - -describe('FieldComponent', () => { - test('it renders disabled if "isDisabled" is true', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] input`).prop('disabled') - ).toBeTruthy(); - }); - - test('it renders loading if "isLoading" is true', () => { - const wrapper = mount( - - ); - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] button`).at(0).simulate('click'); - expect( - wrapper - .find(`EuiComboBoxOptionsList[data-test-subj="fieldAutocompleteComboBox-optionsList"]`) - .prop('isLoading') - ).toBeTruthy(); - }); - - test('it allows user to clear values if "isClearable" is true', () => { - const wrapper = mount( - - ); - - expect( - wrapper - .find(`[data-test-subj="comboBoxInput"]`) - .hasClass('euiComboBox__inputWrap-isClearable') - ).toBeTruthy(); - }); - - test('it correctly displays selected field', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] EuiComboBoxPill`).at(0).text() - ).toEqual('machine.os.raw'); - }); - - test('it invokes "onChange" when option selected', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ label: 'machine.os' }]); - - expect(mockOnChange).toHaveBeenCalledWith([ - { - aggregatable: true, - count: 0, - esTypes: ['text'], - name: 'machine.os', - readFromDocValues: false, - scripted: false, - searchable: true, - type: 'string', - }, - ]); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx deleted file mode 100644 index a175a9b847c715..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx +++ /dev/null @@ -1,146 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useMemo, useCallback } from 'react'; -import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; - -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { getGenericComboBoxProps } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; - -interface OperatorProps { - placeholder: string; - selectedField: IFieldType | undefined; - indexPattern: IIndexPattern | undefined; - isLoading: boolean; - isDisabled: boolean; - isClearable: boolean; - fieldTypeFilter?: string[]; - fieldInputWidth?: number; - isRequired?: boolean; - onChange: (a: IFieldType[]) => void; -} - -/** - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * NOTE: This has deviated from the copy and will have to be reconciled. - */ -export const FieldComponent: React.FC = ({ - placeholder, - selectedField, - indexPattern, - isLoading = false, - isDisabled = false, - isClearable = false, - isRequired = false, - fieldTypeFilter = [], - fieldInputWidth, - onChange, -}): JSX.Element => { - const [touched, setIsTouched] = useState(false); - - const { availableFields, selectedFields } = useMemo( - () => getComboBoxFields(indexPattern, selectedField, fieldTypeFilter), - [indexPattern, selectedField, fieldTypeFilter] - ); - - const { comboOptions, labels, selectedComboOptions } = useMemo( - () => getComboBoxProps({ availableFields, selectedFields }), - [availableFields, selectedFields] - ); - - const handleValuesChange = useCallback( - (newOptions: EuiComboBoxOptionOption[]): void => { - const newValues: IFieldType[] = newOptions.map( - ({ label }) => availableFields[labels.indexOf(label)] - ); - onChange(newValues); - }, - [availableFields, labels, onChange] - ); - - const handleTouch = useCallback((): void => { - setIsTouched(true); - }, [setIsTouched]); - - return ( - - ); -}; - -FieldComponent.displayName = 'Field'; - -interface ComboBoxFields { - availableFields: IFieldType[]; - selectedFields: IFieldType[]; -} - -const getComboBoxFields = ( - indexPattern: IIndexPattern | undefined, - selectedField: IFieldType | undefined, - fieldTypeFilter: string[] -): ComboBoxFields => { - const existingFields = getExistingFields(indexPattern); - const selectedFields = getSelectedFields(selectedField); - const availableFields = getAvailableFields(existingFields, selectedFields, fieldTypeFilter); - - return { availableFields, selectedFields }; -}; - -const getComboBoxProps = (fields: ComboBoxFields): GetGenericComboBoxPropsReturn => { - const { availableFields, selectedFields } = fields; - - return getGenericComboBoxProps({ - options: availableFields, - selectedOptions: selectedFields, - getLabel: (field) => field.name, - }); -}; - -const getExistingFields = (indexPattern: IIndexPattern | undefined): IFieldType[] => { - return indexPattern != null ? indexPattern.fields : []; -}; - -const getSelectedFields = (selectedField: IFieldType | undefined): IFieldType[] => { - return selectedField ? [selectedField] : []; -}; - -const getAvailableFields = ( - existingFields: IFieldType[], - selectedFields: IFieldType[], - fieldTypeFilter: string[] -): IFieldType[] => { - const fieldsByName = new Map(); - - existingFields.forEach((f) => fieldsByName.set(f.name, f)); - selectedFields.forEach((f) => fieldsByName.set(f.name, f)); - - const uniqueFields = Array.from(fieldsByName.values()); - - if (fieldTypeFilter.length > 0) { - return uniqueFields.filter(({ type }) => fieldTypeFilter.includes(type)); - } - - return uniqueFields; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx deleted file mode 100644 index 38d103fe651301..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ /dev/null @@ -1,425 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; -import { EuiSuperSelect, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { act } from '@testing-library/react'; - -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { AutocompleteFieldMatchComponent } from './field_value_match'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -jest.mock('./hooks/use_field_value_autocomplete'); - -describe('AutocompleteFieldMatchComponent', () => { - let wrapper: ReactWrapper; - - const getValueSuggestionsMock = jest - .fn() - .mockResolvedValue([false, true, ['value 3', 'value 4'], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - true, - ['value 1', 'value 2'], - getValueSuggestionsMock, - ]); - }); - - afterEach(() => { - jest.clearAllMocks(); - wrapper.unmount(); - }); - - test('it renders row label if one passed in', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchLabel"] label').at(0).text() - ).toEqual('Row Label'); - }); - - test('it renders disabled if "isDisabled" is true', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] input').prop('disabled') - ).toBeTruthy(); - }); - - test('it renders loading if "isLoading" is true', () => { - wrapper = mount( - - ); - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] button').at(0).simulate('click'); - expect( - wrapper - .find('EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteMatch-optionsList"]') - .prop('isLoading') - ).toBeTruthy(); - }); - - test('it allows user to clear values if "isClearable" is true', () => { - wrapper = mount( - - ); - - expect( - wrapper - .find('[data-test-subj="comboBoxInput"]') - .hasClass('euiComboBox__inputWrap-isClearable') - ).toBeTruthy(); - }); - - test('it correctly displays selected value', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] EuiComboBoxPill').at(0).text() - ).toEqual('126.45.211.34'); - }); - - test('it invokes "onChange" when new value created', async () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onCreateOption: (a: string) => void; - }).onCreateOption('126.45.211.34'); - - expect(mockOnChange).toHaveBeenCalledWith('126.45.211.34'); - }); - - test('it invokes "onChange" when new value selected', async () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ label: 'value 1' }]); - - expect(mockOnChange).toHaveBeenCalledWith('value 1'); - }); - - test('it refreshes autocomplete with search query when new value searched', () => { - wrapper = mount( - - ); - act(() => { - ((wrapper.find(EuiComboBox).props() as unknown) as { - onSearchChange: (a: string) => void; - }).onSearchChange('value 1'); - }); - - expect(useFieldValueAutocomplete).toHaveBeenCalledWith({ - selectedField: getField('machine.os.raw'), - operatorType: 'match', - query: 'value 1', - fieldValue: '', - indexPattern: { - id: '1234', - title: 'logstash-*', - fields, - }, - }); - }); - - describe('boolean type', () => { - const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - false, - [], - valueSuggestionsMock, - ]); - }); - - test('it displays only two options - "true" or "false"', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').exists() - ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').at(0).prop('options') - ).toEqual([ - { - inputDisplay: 'true', - value: 'true', - }, - { - inputDisplay: 'false', - value: 'false', - }, - ]); - }); - - test('it invokes "onChange" with "true" when selected', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiSuperSelect).props() as unknown) as { - onChange: (a: string) => void; - }).onChange('true'); - - expect(mockOnChange).toHaveBeenCalledWith('true'); - }); - - test('it invokes "onChange" with "false" when selected', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiSuperSelect).props() as unknown) as { - onChange: (a: string) => void; - }).onChange('false'); - - expect(mockOnChange).toHaveBeenCalledWith('false'); - }); - }); - - describe('number type', () => { - const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - false, - [], - valueSuggestionsMock, - ]); - }); - - test('it number input when field type is number', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valueAutocompleteFieldMatchNumber"]').exists() - ).toBeTruthy(); - }); - - test('it invokes "onChange" with numeric value when inputted', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - wrapper - .find('[data-test-subj="valueAutocompleteFieldMatchNumber"] input') - .at(0) - .simulate('change', { target: { value: '8' } }); - - expect(mockOnChange).toHaveBeenCalledWith('8'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx deleted file mode 100644 index 21d1d9b4b31aa1..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ /dev/null @@ -1,285 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useMemo, useState, useEffect } from 'react'; -import { - EuiSuperSelect, - EuiFormRow, - EuiFieldNumber, - EuiComboBoxOptionOption, - EuiComboBox, -} from '@elastic/eui'; -import { uniq } from 'lodash'; - -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; -import { paramIsValid, getGenericComboBoxProps } from './helpers'; - -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - -interface AutocompleteFieldMatchProps { - placeholder: string; - selectedField: IFieldType | undefined; - selectedValue: string | undefined; - indexPattern: IIndexPattern | undefined; - isLoading: boolean; - isDisabled: boolean; - isClearable: boolean; - isRequired?: boolean; - fieldInputWidth?: number; - rowLabel?: string; - onChange: (arg: string) => void; - onError?: (arg: boolean) => void; -} - -/** - * There is a copy of this within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const AutocompleteFieldMatchComponent: React.FC = ({ - placeholder, - rowLabel, - selectedField, - selectedValue, - indexPattern, - isLoading, - isDisabled = false, - isClearable = false, - isRequired = false, - fieldInputWidth, - onChange, - onError, -}): JSX.Element => { - const [searchQuery, setSearchQuery] = useState(''); - const [touched, setIsTouched] = useState(false); - const [error, setError] = useState(undefined); - const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({ - selectedField, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: selectedValue, - query: searchQuery, - indexPattern, - }); - const getLabel = useCallback((option: string): string => option, []); - const optionsMemo = useMemo((): string[] => { - const valueAsStr = String(selectedValue); - return selectedValue != null && selectedValue.trim() !== '' - ? uniq([valueAsStr, ...suggestions]) - : suggestions; - }, [suggestions, selectedValue]); - const selectedOptionsMemo = useMemo((): string[] => { - const valueAsStr = String(selectedValue); - return selectedValue ? [valueAsStr] : []; - }, [selectedValue]); - - const handleError = useCallback( - (err: string | undefined): void => { - setError((existingErr): string | undefined => { - const oldErr = existingErr != null; - const newErr = err != null; - if (oldErr !== newErr && onError != null) { - onError(newErr); - } - - return err; - }); - }, - [setError, onError] - ); - - const { comboOptions, labels, selectedComboOptions } = useMemo( - (): GetGenericComboBoxPropsReturn => - getGenericComboBoxProps({ - options: optionsMemo, - selectedOptions: selectedOptionsMemo, - getLabel, - }), - [optionsMemo, selectedOptionsMemo, getLabel] - ); - - const handleValuesChange = useCallback( - (newOptions: EuiComboBoxOptionOption[]): void => { - const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]); - handleError(undefined); - onChange(newValue ?? ''); - }, - [handleError, labels, onChange, optionsMemo] - ); - - const handleSearchChange = useCallback( - (searchVal: string): void => { - if (searchVal !== '' && selectedField != null) { - const err = paramIsValid(searchVal, selectedField, isRequired, touched); - handleError(err); - - setSearchQuery(searchVal); - } - }, - [handleError, isRequired, selectedField, touched] - ); - - const handleCreateOption = useCallback( - (option: string): boolean | undefined => { - const err = paramIsValid(option, selectedField, isRequired, touched); - handleError(err); - - if (err != null) { - // Explicitly reject the user's input - return false; - } else { - onChange(option); - } - }, - [isRequired, onChange, selectedField, touched, handleError] - ); - - const handleNonComboBoxInputChange = (event: React.ChangeEvent): void => { - const newValue = event.target.value; - onChange(newValue); - }; - - const handleBooleanInputChange = (newOption: string): void => { - onChange(newOption); - }; - - const setIsTouchedValue = useCallback((): void => { - setIsTouched(true); - - const err = paramIsValid(selectedValue, selectedField, isRequired, true); - handleError(err); - }, [setIsTouched, handleError, selectedValue, selectedField, isRequired]); - - const inputPlaceholder = useMemo((): string => { - if (isLoading || isLoadingSuggestions) { - return i18n.LOADING; - } else if (selectedField == null) { - return i18n.SELECT_FIELD_FIRST; - } else { - return placeholder; - } - }, [isLoading, selectedField, isLoadingSuggestions, placeholder]); - - const isLoadingState = useMemo((): boolean => isLoading || isLoadingSuggestions, [ - isLoading, - isLoadingSuggestions, - ]); - - useEffect((): void => { - setError(undefined); - if (onError != null) { - onError(false); - } - }, [selectedField, onError]); - - const defaultInput = useMemo((): JSX.Element => { - return ( - - - - ); - }, [ - comboOptions, - error, - fieldInputWidth, - handleCreateOption, - handleSearchChange, - handleValuesChange, - inputPlaceholder, - isClearable, - isDisabled, - isLoadingState, - rowLabel, - selectedComboOptions, - selectedField, - setIsTouchedValue, - ]); - - if (!isSuggestingValues && selectedField != null) { - switch (selectedField.type) { - case 'number': - return ( - - 0 - ? parseFloat(selectedValue) - : selectedValue ?? '' - } - onChange={handleNonComboBoxInputChange} - data-test-subj="valueAutocompleteFieldMatchNumber" - style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}} - fullWidth - /> - - ); - case 'boolean': - return ( - - - - ); - default: - return defaultInput; - } - } else { - return defaultInput; - } -}; - -AutocompleteFieldMatchComponent.displayName = 'AutocompleteFieldMatch'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts deleted file mode 100644 index 1618de245365dc..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ /dev/null @@ -1,223 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import '../../../common/mock/match_media'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -import * as i18n from './translations'; -import { checkEmptyValue, paramIsValid, getGenericComboBoxProps } from './helpers'; - -describe('helpers', () => { - // @ts-ignore - moment.suppressDeprecationWarnings = true; - - describe('#checkEmptyValue', () => { - test('returns no errors if no field has been selected', () => { - const isValid = checkEmptyValue('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = checkEmptyValue('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns null if input value is not empty string or undefined', () => { - const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true); - - expect(isValid).toBeNull(); - }); - }); - - describe('#paramIsValid', () => { - test('returns no errors if no field has been selected', () => { - const isValid = paramIsValid('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = paramIsValid('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type date and value is valid', () => { - const isValid = paramIsValid( - '1994-11-05T08:15:30-05:00', - getField('@timestamp'), - false, - true - ); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if filed is of type date and value is not valid', () => { - const isValid = paramIsValid('1593478826', getField('@timestamp'), false, true); - - expect(isValid).toEqual(i18n.DATE_ERR); - }); - - test('returns no errors if field is of type number and value is an integer', () => { - const isValid = paramIsValid('4', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a float', () => { - const isValid = paramIsValid('4.3', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a long', () => { - const isValid = paramIsValid('-9223372036854775808', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if field is of type number and value is "hello"', () => { - const isValid = paramIsValid('hello', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - - test('returns errors if field is of type number and value is "123abc"', () => { - const isValid = paramIsValid('123abc', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - }); - - describe('#getGenericComboBoxProps', () => { - test('it returns empty arrays if "options" is empty array', () => { - const result = getGenericComboBoxProps({ - options: [], - selectedOptions: ['option1'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); - }); - - test('it returns formatted props if "options" array is not empty', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: [], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it does not return "selectedOptions" items that do not appear in "options"', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option4'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it return "selectedOptions" items that do appear in "options"', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option2'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [ - { - label: 'option2', - }, - ], - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts deleted file mode 100644 index 890f1e67558349..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ /dev/null @@ -1,119 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import dateMath from '@elastic/datemath'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; - -import { IFieldType } from '../../../../../../../src/plugins/data/common'; - -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - -/** - * Determines if empty value is ok - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const checkEmptyValue = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined | null => { - if (isRequired && touched && (param == null || param.trim() === '')) { - return i18n.FIELD_REQUIRED_ERR; - } - - if ( - field == null || - (isRequired && !touched) || - (!isRequired && (param == null || param === '')) - ) { - return undefined; - } - - return null; -}; - -/** - * Very basic validation for values - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * @param param the value being checked - * @param field the selected field - * @param isRequired whether or not an empty value is allowed - * @param touched has field been touched by user - * @returns undefined if valid, string with error message if invalid - */ -export const paramIsValid = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined => { - if (field == null) { - return undefined; - } - - const emptyValueError = checkEmptyValue(param, field, isRequired, touched); - if (emptyValueError !== null) { - return emptyValueError; - } - - switch (field.type) { - case 'date': - const moment = dateMath.parse(param ?? ''); - const isDate = Boolean(moment && moment.isValid()); - return isDate ? undefined : i18n.DATE_ERR; - case 'number': - const isNum = param != null && param.trim() !== '' && !isNaN(+param); - return isNum ? undefined : i18n.NUMBER_ERR; - default: - return undefined; - } -}; - -/** - * Determines the options, selected values and option labels for EUI combo box - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * @param options options user can select from - * @param selectedOptions user selection if any - * @param getLabel helper function to know which property to use for labels - */ -export function getGenericComboBoxProps({ - options, - selectedOptions, - getLabel, -}: { - options: T[]; - selectedOptions: T[]; - getLabel: (value: T) => string; -}): GetGenericComboBoxPropsReturn { - const newLabels = options.map(getLabel); - const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); - const newSelectedComboOptions = selectedOptions - .map(getLabel) - .filter((option) => { - return newLabels.indexOf(option) !== -1; - }) - .map((option) => { - return newComboOptions[newLabels.indexOf(option)]; - }); - - return { - comboOptions: newComboOptions, - labels: newLabels, - selectedComboOptions: newSelectedComboOptions, - }; -} diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts deleted file mode 100644 index e0bdbf2603dc32..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ /dev/null @@ -1,325 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act, renderHook } from '@testing-library/react-hooks'; - -import { - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn, - useFieldValueAutocomplete, -} from './use_field_value_autocomplete'; -import { useKibana } from '../../../../common/lib/kibana'; -import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; -import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; - -jest.mock('../../../../common/lib/kibana'); - -describe('useFieldValueAutocomplete', () => { - const onErrorMock = jest.fn(); - const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); - - beforeEach(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: getValueSuggestionsMock, - }, - }, - }, - }); - }); - - afterEach(() => { - onErrorMock.mockClear(); - getValueSuggestionsMock.mockClear(); - }); - - test('initializes hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: undefined, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: undefined, - query: '', - }) - ); - await waitForNextUpdate(); - - expect(result.current).toEqual([false, true, [], result.current[3]]); - }); - }); - - test('does not call autocomplete service if "operatorType" is "exists"', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('machine.os'), - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('does not call autocomplete service if "selectedField" is undefined', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: undefined, - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('does not call autocomplete service if "indexPattern" is undefined', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('machine.os'), - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: undefined, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('it uses full path name for nested fields to fetch suggestions', async () => { - const suggestionsMock = jest.fn().mockResolvedValue([]); - - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: suggestionsMock, - }, - }, - }, - }); - await act(async () => { - const signal = new AbortController().signal; - const { waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: { ...getField('nestedField.child'), name: 'child' }, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(suggestionsMock).toHaveBeenCalledWith({ - field: { ...getField('nestedField.child'), name: 'nestedField.child' }, - indexPattern: { - fields: [ - { - aggregatable: true, - esTypes: ['integer'], - filterable: true, - name: 'response', - searchable: true, - type: 'number', - }, - ], - id: '1234', - title: 'logstash-*', - }, - query: '', - signal, - }); - }); - }); - - test('returns "isSuggestingValues" of false if field type is boolean', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('ssl'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, false, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns "isSuggestingValues" of false to note that autocomplete service is not in use if no autocomplete suggestions available', async () => { - const suggestionsMock = jest.fn().mockResolvedValue([]); - - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: suggestionsMock, - }, - }, - }, - }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('bytes'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, false, [], result.current[3]]; - - expect(suggestionsMock).toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns suggestions', async () => { - await act(async () => { - const signal = new AbortController().signal; - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('@tags'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [ - false, - true, - ['value 1', 'value 2'], - result.current[3], - ]; - - expect(getValueSuggestionsMock).toHaveBeenCalledWith({ - field: getField('@tags'), - indexPattern: stubIndexPatternWithFields, - query: '', - signal, - }); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns new suggestions on subsequent calls', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('@tags'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current[3]).not.toBeNull(); - - // Added check for typescripts sake, if null, - // would not reach below logic as test would stop above - if (result.current[3] != null) { - result.current[3]({ - fieldSelected: getField('@tags'), - value: 'hello', - patterns: stubIndexPatternWithFields, - searchQuery: '', - }); - } - - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [ - false, - true, - ['value 1', 'value 2'], - result.current[3], - ]; - - expect(getValueSuggestionsMock).toHaveBeenCalledTimes(2); - expect(result.current).toEqual(expectedResult); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts deleted file mode 100644 index 0fc4a663b7e11b..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ /dev/null @@ -1,123 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState, useRef } from 'react'; -import { debounce } from 'lodash'; - -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { useKibana } from '../../../../common/lib/kibana'; - -interface FuncArgs { - fieldSelected: IFieldType | undefined; - value: string | string[] | undefined; - searchQuery: string; - patterns: IIndexPattern | undefined; -} - -type Func = (args: FuncArgs) => void; - -export type UseFieldValueAutocompleteReturn = [boolean, boolean, string[], Func | null]; - -export interface UseFieldValueAutocompleteProps { - selectedField: IFieldType | undefined; - operatorType: OperatorTypeEnum; - fieldValue: string | string[] | undefined; - query: string; - indexPattern: IIndexPattern | undefined; -} - -/** - * Hook for using the field value autocomplete service - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const useFieldValueAutocomplete = ({ - selectedField, - operatorType, - fieldValue, - query, - indexPattern, -}: UseFieldValueAutocompleteProps): UseFieldValueAutocompleteReturn => { - const { services } = useKibana(); - const [isLoading, setIsLoading] = useState(false); - const [isSuggestingValues, setIsSuggestingValues] = useState(true); - const [suggestions, setSuggestions] = useState([]); - const updateSuggestions = useRef(null); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchSuggestions = debounce( - async ({ fieldSelected, value, searchQuery, patterns }: FuncArgs) => { - try { - if (isSubscribed) { - if (fieldSelected == null || patterns == null) { - return; - } - - if (fieldSelected.type === 'boolean') { - setIsSuggestingValues(false); - return; - } - - setIsLoading(true); - - const field = - fieldSelected.subType != null && fieldSelected.subType.nested != null - ? { - ...fieldSelected, - name: `${fieldSelected.subType.nested.path}.${fieldSelected.name}`, - } - : fieldSelected; - - const newSuggestions = await services.data.autocomplete.getValueSuggestions({ - indexPattern: patterns, - field, - query: searchQuery, - signal: abortCtrl.signal, - }); - - if (newSuggestions.length === 0) { - setIsSuggestingValues(false); - } - - setIsLoading(false); - setSuggestions([...newSuggestions]); - } - } catch (error) { - if (isSubscribed) { - setSuggestions([]); - setIsLoading(false); - } - } - }, - 500 - ); - - if (operatorType !== OperatorTypeEnum.EXISTS) { - fetchSuggestions({ - fieldSelected: selectedField, - value: fieldValue, - searchQuery: query, - patterns: indexPattern, - }); - } - - updateSuggestions.current = fetchSuggestions; - - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [services.data.autocomplete, selectedField, operatorType, fieldValue, indexPattern, query]); - - return [isLoading, isSuggestingValues, suggestions, updateSuggestions.current]; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md b/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md deleted file mode 100644 index 2bf1867c008d23..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md +++ /dev/null @@ -1,122 +0,0 @@ -# Autocomplete Fields - -Need an input that shows available index fields? Or an input that autocompletes based on a selected indexPattern field? Bingo! That's what these components are for. They are generalized enough so that they can be reused throughout and repurposed based on your needs. - -All three of the available components rely on Eui's combo box. - -## useFieldValueAutocomplete - -This hook uses the kibana `services.data.autocomplete.getValueSuggestions()` service to return possible autocomplete fields based on the passed in `indexPattern` and `selectedField`. - -## FieldComponent - -This component can be used to display available indexPattern fields. It requires an indexPattern to be passed in and will show an error state if value is not one of the available indexPattern fields. Users will be able to select only one option. - -The `onChange` handler is passed `IFieldType[]`. - -```js - -``` - -## OperatorComponent - -This component can be used to display available operators. If you want to pass in your own operators, you can use `operatorOptions` prop. If a `operatorOptions` is provided, those will be used and it will ignore any of the built in logic that determines which operators to show. The operators within `operatorOptions` will still need to be of type `OperatorOption`. - -If no `operatorOptions` is provided, then the following behavior is observed: - -- if `selectedField` type is `boolean`, only `is`, `is not`, `exists`, `does not exist` operators will show -- if `selectedField` type is `nested`, only `is` operator will show -- if not one of the above, all operators will show (see `operators.ts`) - -The `onChange` handler is passed `OperatorOption[]`. - -```js - -``` - -## AutocompleteFieldExistsComponent - -This field value component is used when the selected operator is `exists` or `does not exist`. When these operators are selected, they are equivalent to using a wildcard. The combo box will be displayed as disabled. - -```js - -``` - -## AutocompleteFieldListsComponent - -This component can be used to display available large value lists - when operator selected is `is in list` or `is not in list`. It relies on hooks from the `lists` plugin. Users can only select one list and an error is shown if value is not one of available lists. - -The `selectedValue` should be the `id` of the selected list. - -This component relies on `selectedField` to render available lists. The reason being that it relies on the `selectedField` type to determine which lists to show as each large value list has a type as well. So if a user selects a field of type `ip`, it will only display lists of type `ip`. - -The `onChange` handler is passed `ListSchema`. - -```js - -``` - -## AutocompleteFieldMatchComponent - -This component can be used to allow users to select one single value. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own value. - -It does some minor validation, assuring that field value is a date if `selectedField` type is `date`, a number if `selectedField` type is `number`, an ip if `selectedField` type is `ip`. - -The `onChange` handler is passed selected `string`. - -```js - -``` - -## AutocompleteFieldMatchAnyComponent - -This component can be used to allow users to select multiple values. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own values. - -It does some minor validation, assuring that field values are a date if `selectedField` type is `date`, numbers if `selectedField` type is `number`, ips if `selectedField` type is `ip`. - -The `onChange` handler is passed selected `string[]`. - -```js - -``` diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts deleted file mode 100644 index 084f4b0698aacd..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts +++ /dev/null @@ -1,34 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const LOADING = i18n.translate('xpack.securitySolution.autocomplete.loadingDescription', { - defaultMessage: 'Loading...', -}); - -export const SELECT_FIELD_FIRST = i18n.translate( - 'xpack.securitySolution.autocomplete.selectField', - { - defaultMessage: 'Please select a field first...', - } -); - -export const FIELD_REQUIRED_ERR = i18n.translate( - 'xpack.securitySolution.autocomplete.fieldRequiredError', - { - defaultMessage: 'Value cannot be empty', - } -); - -export const NUMBER_ERR = i18n.translate('xpack.securitySolution.autocomplete.invalidNumberError', { - defaultMessage: 'Not a valid number', -}); - -export const DATE_ERR = i18n.translate('xpack.securitySolution.autocomplete.invalidDateError', { - defaultMessage: 'Not a valid date', -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts deleted file mode 100644 index 07f1903fb70e1c..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts +++ /dev/null @@ -1,14 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiComboBoxOptionOption } from '@elastic/eui'; - -export interface GetGenericComboBoxPropsReturn { - comboOptions: EuiComboBoxOptionOption[]; - labels: string[]; - selectedComboOptions: EuiComboBoxOptionOption[]; -} diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx index 49cfd841b7f8a6..49bd7824d61008 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx @@ -9,8 +9,8 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import { IFieldType, IndexPattern } from '../../../../../../../src/plugins/data/common'; -import { FieldComponent } from '../autocomplete/field'; import { FormattedEntry, Entry } from './types'; import * as i18n from './translations'; import { getEntryOnFieldChange, getEntryOnThreatFieldChange } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx index 503a568f137442..d74b1d0502acf5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx @@ -7,8 +7,8 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFormRow } from '@elastic/eui'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index c02f7992a9b926..eef18a502c2704 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -20,10 +20,10 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import { RiskScoreMapping } from '@kbn/securitysolution-io-ts-alerting-types'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 8b8c9441e7eae7..d4fbdc31fbcae9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -24,6 +24,11 @@ import { SeverityMapping, SeverityMappingItem, } from '@kbn/securitysolution-io-ts-alerting-types'; +import { + FieldComponent, + AutocompleteFieldMatchComponent, +} from '@kbn/securitysolution-autocomplete'; + import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; @@ -32,8 +37,7 @@ import { IFieldType, IIndexPattern, } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; -import { AutocompleteFieldMatchComponent } from '../../../../common/components/autocomplete/field_value_match'; +import { useKibana } from '../../../../common/lib/kibana'; const NestedContent = styled.div` margin-left: 24px; @@ -68,6 +72,7 @@ export const SeverityField = ({ isDisabled, options, }: SeverityFieldProps) => { + const { services } = useKibana(); const { value, isMappingChecked, mapping } = field.value; const { setValue } = field; @@ -254,6 +259,7 @@ export const SeverityField = ({