From cd667d06bcd97b4f9eaefb771db6d9901099c8fd Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 22 Jul 2021 12:44:54 -0600 Subject: [PATCH] [Security Solutions][Detection Engine] Creates an autocomplete package and moves duplicate code between lists and security_solution there (#105382) ## Summary Creates an autocomplete package from `lists` and removes duplicate code between `lists` and `security_solutions` * Consolidates different PR's where we were changing different parts of autocomplete in different ways. * Existing Cypress tests should cover any mistakes hopefully Manual Testing: * Ensure this bug does not crop up again https://github.com/elastic/kibana/pull/87004 * Make sure that the exception list autocomplete looks alright ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .i18nrc.json | 1 + .../monorepo-packages.asciidoc | 1 + package.json | 1 + packages/BUILD.bazel | 1 + .../BUILD.bazel | 125 ++++++ .../README.md | 0 .../babel.config.js | 19 + .../jest.config.js | 13 + .../package.json | 10 + .../react/package.json | 5 + .../src/autocomplete/index.mock.ts | 15 + .../src/check_empty_value/index.test.ts | 49 ++ .../src/check_empty_value/index.ts | 37 ++ .../src/field/index.test.tsx | 16 +- .../src/field/index.tsx | 23 +- .../src/field_value_exists/index.test.tsx | 7 +- .../src/field_value_exists/index.tsx | 5 +- .../src/field_value_lists/index.test.tsx | 29 +- .../src/field_value_lists/index.tsx | 20 +- .../src/field_value_match/index.test.tsx | 24 +- .../src/field_value_match/index.tsx | 55 +-- .../src/field_value_match_any/index.test.tsx | 27 +- .../src/field_value_match_any/index.tsx | 28 +- .../src/fields/index.mock.ts | 313 +++++++++++++ .../src/filter_field_to_list/index.test.ts | 79 ++++ .../src/filter_field_to_list/index.ts | 29 ++ .../index.test.tsx | 97 ++++ .../src/get_generic_combo_box_props/index.ts | 48 ++ .../src/get_operators/index.test.ts | 53 +++ .../src/get_operators/index.ts | 38 ++ .../src/hooks/index.ts | 8 + .../index.test.ts | 38 +- .../use_field_value_autocomplete/index.ts | 19 +- .../src/index.ts | 19 + .../src/list_schema/index.mock.ts | 51 +++ .../src/operator/index.test.tsx | 12 +- .../src/operator/index.tsx | 16 +- .../src/param_is_valid/index.test.ts | 102 +++++ .../src/param_is_valid/index.ts | 52 +++ .../src/translations/index.ts | 29 ++ .../src/type_match/index.test.ts | 59 +++ .../src/type_match/index.ts | 27 ++ .../tsconfig.browser.json | 23 + .../tsconfig.json | 16 + .../components/autocomplete/helpers.test.ts | 388 ---------------- .../components/autocomplete/helpers.ts | 183 -------- .../components/autocomplete/index.tsx | 13 - .../components/autocomplete/translations.ts | 28 -- .../components/autocomplete/types.ts | 14 - .../components/builder/entry_renderer.tsx | 14 +- .../components/autocomplete/field.test.tsx | 146 ------ .../common/components/autocomplete/field.tsx | 146 ------ .../autocomplete/field_value_match.test.tsx | 425 ------------------ .../autocomplete/field_value_match.tsx | 285 ------------ .../components/autocomplete/helpers.test.ts | 223 --------- .../common/components/autocomplete/helpers.ts | 119 ----- .../use_field_value_autocomplete.test.ts | 325 -------------- .../hooks/use_field_value_autocomplete.ts | 123 ----- .../common/components/autocomplete/readme.md | 122 ----- .../components/autocomplete/translations.ts | 34 -- .../common/components/autocomplete/types.ts | 14 - .../components/threat_match/entry_item.tsx | 2 +- .../rules/autocomplete_field/index.tsx | 2 +- .../rules/risk_score_mapping/index.tsx | 2 +- .../rules/severity_mapping/index.tsx | 10 +- .../translations/translations/ja-JP.json | 16 +- .../translations/translations/zh-CN.json | 12 +- yarn.lock | 4 + 68 files changed, 1524 insertions(+), 2765 deletions(-) create mode 100644 packages/kbn-securitysolution-autocomplete/BUILD.bazel rename {x-pack/plugins/lists/public/exceptions/components/autocomplete => packages/kbn-securitysolution-autocomplete}/README.md (100%) create mode 100644 packages/kbn-securitysolution-autocomplete/babel.config.js create mode 100644 packages/kbn-securitysolution-autocomplete/jest.config.js create mode 100644 packages/kbn-securitysolution-autocomplete/package.json create mode 100644 packages/kbn-securitysolution-autocomplete/react/package.json create mode 100644 packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx => packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx (92%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx => packages/kbn-securitysolution-autocomplete/src/field/index.tsx (87%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx => packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx (70%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx => packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx (83%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx => packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx (88%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx => packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx (80%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx => packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx (95%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx => packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx (85%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx => packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx (91%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx => packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx (86%) create mode 100644 packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx create mode 100644 packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/hooks/index.ts rename x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts => packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts (92%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts => packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts (81%) create mode 100644 packages/kbn-securitysolution-autocomplete/src/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts rename x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx => packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx (95%) rename x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx => packages/kbn-securitysolution-autocomplete/src/operator/index.tsx (80%) create mode 100644 packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.test.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/translations/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/type_match/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/tsconfig.browser.json create mode 100644 packages/kbn-securitysolution-autocomplete/tsconfig.json delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts diff --git a/.i18nrc.json b/.i18nrc.json index 0ee1e55ed62c6..ad32edb67b83f 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 b656405b173d8..0b635df68aca4 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 9034638c689a4..f7856b9f92e74 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,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 0719357b6df35..778a7c7a0f2d4 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -36,6 +36,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 0000000000000..8e403a215d81d --- /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 0000000000000..b4a118df51af5 --- /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 0000000000000..9b14447c98366 --- /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 0000000000000..5cfd18b63256a --- /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 0000000000000..c5f222b5843ac --- /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 0000000000000..444a033b4887b --- /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 0000000000000..c36184e5c5ba1 --- /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 0000000000000..894f233f73a5a --- /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 416852b469a79..08f55cef89b66 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 b6300581f12dd..c4c07aff909e4 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 ff70204e53483..37a16406e65a3 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 a338ce6a27d6c..6fcf8ddf74b03 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 047f8ef33c8c0..4064ff11962bd 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 c1ffb008e8563..d695088245622 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 8dbe8f223ae5b..8199967489515 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 8aa1f18b695a0..a3ca97874908e 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 e5a5e76f8cc5d..338c4baa8bc6f 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 0000000000000..5938ed34547a1 --- /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 0000000000000..1022849ffda36 --- /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 0000000000000..b2e48c25f9b51 --- /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 0000000000000..63a94be1271a7 --- /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 0000000000000..0fba3c39344b8 --- /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 0000000000000..e473df104fa6a --- /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 0000000000000..39d2779e2dc44 --- /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 0000000000000..cc5a37bfc46f0 --- /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 0335ffa55d2a2..534daa021cf4a 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 63d3925d6d64d..b4dec1615e3ed 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 0000000000000..5fcb3f954189a --- /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 0000000000000..fb629ad2f946e --- /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 dadde8800b67f..fed7007b49636 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 0000000000000..5b596b4b62408 --- /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 0000000000000..35d6531be51bd --- /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 0000000000000..4694313720c79 --- /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 0000000000000..d5476f3b32b49 --- /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 0000000000000..bab7b18c59cfd --- /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 0000000000000..bf402e93ffd69 --- /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 21764c6f459d8..0000000000000 --- 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 975416e272227..0000000000000 --- 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 1623683f25ed5..0000000000000 --- 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 065239246d329..0000000000000 --- 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 07f1903fb70e1..0000000000000 --- 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 c54da89766d76..d7741b3fe0ff1 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 79e6fe5506b84..0000000000000 --- 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 a175a9b847c71..0000000000000 --- 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 38d103fe65130..0000000000000 --- 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 21d1d9b4b31aa..0000000000000 --- 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 1618de245365d..0000000000000 --- 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 890f1e6755834..0000000000000 --- 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 e0bdbf2603dc3..0000000000000 --- 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 0fc4a663b7e11..0000000000000 --- 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 2bf1867c008d2..0000000000000 --- 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 084f4b0698aac..0000000000000 --- 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 07f1903fb70e1..0000000000000 --- 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 49cfd841b7f8a..49bd7824d6100 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 51404f65dc7d4..16caed9086e61 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'; 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 c02f7992a9b92..eef18a502c270 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 8b8c9441e7eae..d4fbdc31fbcae 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 = ({