From b2567e7a2a44aac9669d0982adfc06e385eb75ac Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Mon, 12 Jul 2021 21:26:32 -0600 Subject: [PATCH 1/8] Creates an auto-complete packaage --- .i18nrc.json | 1 + .../monorepo-packages.asciidoc | 1 + package.json | 1 + packages/BUILD.bazel | 1 + .../BUILD.bazel | 92 ++++++ .../README.md | 122 +++++++ .../jest.config.js | 13 + .../package.json | 9 + .../src/check_empty_value/index.test.ts | 14 + .../src/check_empty_value/index.ts | 41 +++ .../src/check_empty_value/translations.ts | 13 + .../src/field/index.test.tsx | 14 + .../src/field/index.tsx | 152 +++++++++ .../src/field_value_match/index.test.tsx | 14 + .../src/field_value_match/index.tsx | 311 ++++++++++++++++++ .../src/field_value_match/translations.ts | 20 ++ .../index.test.tsx | 97 ++++++ .../src/get_generic_combo_box_props/index.ts | 48 +++ .../src/hooks/index.ts | 8 + .../index.test.ts | 14 + .../use_field_value_autocomplete/index.ts | 126 +++++++ .../src/index.ts | 12 + .../src/param_is_valid/index.test.ts | 14 + .../src/param_is_valid/index.ts | 52 +++ .../src/param_is_valid/translations.ts | 17 + .../tsconfig.json | 14 + yarn.lock | 4 + 27 files changed, 1225 insertions(+) create mode 100644 packages/kbn-securitysolution-autocomplete/BUILD.bazel create mode 100644 packages/kbn-securitysolution-autocomplete/README.md 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/src/check_empty_value/index.test.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx create mode 100644 packages/kbn-securitysolution-autocomplete/src/field/index.tsx create mode 100644 packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx create mode 100644 packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx create mode 100644 packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.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/hooks/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/index.ts 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/param_is_valid/translations.ts create mode 100644 packages/kbn-securitysolution-autocomplete/tsconfig.json diff --git a/.i18nrc.json b/.i18nrc.json index 390e5e917d08e7..9c02a204d4ca73 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -1,5 +1,6 @@ { "paths": { + "autocomplete": "packages/kbn-securitysolution-autocomplete/src", "console": "src/plugins/console", "core": "src/core", "discover": "src/plugins/discover", diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index b656405b173d82..0b635df68aca4d 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -91,6 +91,7 @@ yarn kbn watch-bazel - @kbn/optimizer - @kbn/plugin-helpers - @kbn/rule-data-utils +- @kbn/securitysolution-autocomplete - @kbn/securitysolution-es-utils - @kbn/securitysolution-hook-utils - @kbn/securitysolution-io-ts-alerting-types diff --git a/package.json b/package.json index 22eedde59c5e7d..73440af42f4cd8 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,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 de7a27fd512769..a5ce3da05d2c83 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -35,6 +35,7 @@ filegroup( "//packages/kbn-plugin-generator:build", "//packages/kbn-plugin-helpers:build", "//packages/kbn-rule-data-utils:build", + "//packages/kbn-securitysolution-autocomplete:build", "//packages/kbn-securitysolution-list-constants:build", "//packages/kbn-securitysolution-io-ts-types:build", "//packages/kbn-securitysolution-io-ts-alerting-types:build", diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel new file mode 100644 index 00000000000000..e8d54b30f11a0d --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -0,0 +1,92 @@ +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.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-i18n", + "//packages/kbn-securitysolution-io-ts-list-types", + "@npm//@elastic/eui", + "@npm//react", + "@npm//resize-observer-polyfill", + "@npm//rxjs", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@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_project( + name = "tsc", + srcs = SRCS, + args = ["--pretty"], + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", + deps = DEPS, +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = DEPS + [":tsc"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ], +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-autocomplete/README.md b/packages/kbn-securitysolution-autocomplete/README.md new file mode 100644 index 00000000000000..fb500ca0761e33 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/README.md @@ -0,0 +1,122 @@ +# 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 + +``` \ No newline at end of file diff --git a/packages/kbn-securitysolution-autocomplete/jest.config.js b/packages/kbn-securitysolution-autocomplete/jest.config.js new file mode 100644 index 00000000000000..9b14447c983660 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-autocomplete'], +}; diff --git a/packages/kbn-securitysolution-autocomplete/package.json b/packages/kbn-securitysolution-autocomplete/package.json new file mode 100644 index 00000000000000..29d6edcec6bee3 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-autocomplete", + "version": "1.0.0", + "description": "Security Solution auto complete", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts new file mode 100644 index 00000000000000..417c2cc952e7a2 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +describe('use_field_value_autocomplete', () => { + test('Tests should be ported', () => { + // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts here once mocks are figured out and kbn package mocks are figured out + expect(true).toBe(true); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts new file mode 100644 index 00000000000000..1ce1967d4c4e84 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts @@ -0,0 +1,41 @@ +/* + * 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 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +/** + * 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; +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts new file mode 100644 index 00000000000000..44709743b3508d --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts @@ -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. + */ + +import { i18n } from '@kbn/i18n'; + +export const FIELD_REQUIRED_ERR = i18n.translate('xpack.autocomplete.fieldRequiredError', { + defaultMessage: 'Value cannot be empty', +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx new file mode 100644 index 00000000000000..c4255823f17d1f --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx @@ -0,0 +1,14 @@ +/* + * 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. + */ + +describe('field', () => { + test('Tests should be ported', () => { + // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx here once mocks are figured out and kbn package mocks are figured out + expect(true).toBe(true); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx new file mode 100644 index 00000000000000..4c2e19a0afb24f --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx @@ -0,0 +1,152 @@ +/* + * 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 React, { useCallback, useMemo, useState } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; + +import { + getGenericComboBoxProps, + GetGenericComboBoxPropsReturn, +} from '../get_generic_combo_box_props'; + +const AS_PLAIN_TEXT = { asPlainText: true }; + +interface OperatorProps { + fieldInputWidth?: number; + fieldTypeFilter?: string[]; + indexPattern: IIndexPattern | undefined; + isClearable: boolean; + isDisabled: boolean; + isLoading: boolean; + isRequired?: boolean; + onChange: (a: IFieldType[]) => void; + placeholder: string; + selectedField: IFieldType | undefined; +} + +export const FieldComponent: React.FC = ({ + fieldInputWidth, + fieldTypeFilter = [], + indexPattern, + isClearable = false, + isDisabled = false, + isLoading = false, + isRequired = false, + onChange, + placeholder, + selectedField, +}): 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]); + + const fieldWidth = useMemo(() => { + return fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}; + }, [fieldInputWidth]); + + 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({ + getLabel: (field) => field.name, + options: availableFields, + selectedOptions: selectedFields, + }); +}; + +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/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx new file mode 100644 index 00000000000000..50f052c689d666 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx @@ -0,0 +1,14 @@ +/* + * 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. + */ + +describe('field_value_match', () => { + test('Tests should be ported', () => { + // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx here once mocks are figured out and kbn package mocks are figured out + expect(true).toBe(true); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx new file mode 100644 index 00000000000000..7f4b91569128b9 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx @@ -0,0 +1,311 @@ +/* + * 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 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'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types +// 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 +// 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' }, + { inputDisplay: 'false', value: 'false' }, +]; + +const SINGLE_SELECTION = { asPlainText: true }; + +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; + autocompleteService: AutocompleteStart; + onChange: (arg: string) => void; + onError?: (arg: boolean) => void; +} + +export const AutocompleteFieldMatchComponent: React.FC = ({ + placeholder, + rowLabel, + selectedField, + selectedValue, + indexPattern, + isLoading, + isDisabled = false, + isClearable = false, + isRequired = false, + fieldInputWidth, + onChange, + onError, + autocompleteService, +}): JSX.Element => { + const [searchQuery, setSearchQuery] = useState(''); + const [touched, setIsTouched] = useState(false); + const [error, setError] = useState(undefined); + const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({ + autocompleteService, + fieldValue: selectedValue, + indexPattern, + operatorType: OperatorTypeEnum.MATCH, + query: searchQuery, + selectedField, + }); + 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({ + getLabel, + options: optionsMemo, + selectedOptions: selectedOptionsMemo, + }), + [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); + return undefined; + } + }, + [isRequired, onChange, selectedField, touched, handleError] + ); + + const handleNonComboBoxInputChange = useCallback( + (event: React.ChangeEvent): void => { + const newValue = event.target.value; + onChange(newValue); + }, + [onChange] + ); + + const handleBooleanInputChange = useCallback( + (newOption: string): void => { + onChange(newOption); + }, + [onChange] + ); + + 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, + ]); + + const fieldInputWidths = useMemo( + () => (fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}), + [fieldInputWidth] + ); + + useEffect((): void => { + setError(undefined); + if (onError != null) { + onError(false); + } + }, [selectedField, onError]); + + const defaultInput = useMemo((): JSX.Element => { + return ( + + + + ); + }, [ + comboOptions, + error, + fieldInputWidths, + 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={fieldInputWidths} + fullWidth + /> + + ); + case 'boolean': + return ( + + + + ); + default: + return defaultInput; + } + } else { + return defaultInput; + } +}; + +AutocompleteFieldMatchComponent.displayName = 'AutocompleteFieldMatch'; diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts b/packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts new file mode 100644 index 00000000000000..0ed97753bbca3e --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts @@ -0,0 +1,20 @@ +/* + * 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('xpack.autocomplete.loadingDescription', { + defaultMessage: 'Loading...', +}); + +export const SELECT_FIELD_FIRST = i18n.translate( + 'xpack.securitySolution.autocomplete.selectField', + { + defaultMessage: 'Please select a field first...', + } +); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx new file mode 100644 index 00000000000000..63a94be1271a7c --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getGenericComboBoxProps } from '.'; + +describe('get_generic_combo_box_props', () => { + test('it returns empty arrays if "options" is empty array', () => { + const result = getGenericComboBoxProps({ + options: [], + selectedOptions: ['option1'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); + }); + + test('it returns formatted props if "options" array is not empty', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: [], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [], + }); + }); + + test('it does not return "selectedOptions" items that do not appear in "options"', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: ['option4'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [], + }); + }); + + test('it return "selectedOptions" items that do appear in "options"', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: ['option2'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [ + { + label: 'option2', + }, + ], + }); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts new file mode 100644 index 00000000000000..5e42500039223e --- /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 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/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts new file mode 100644 index 00000000000000..cc5a37bfc46f07 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export * from './use_field_value_autocomplete'; diff --git a/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts new file mode 100644 index 00000000000000..84fd3c51649161 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +describe('use_field_value_autocomplete', () => { + test('Tests should be ported', () => { + // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts here once mocks are figured out and kbn package mocks are figured out + expect(true).toBe(true); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts new file mode 100644 index 00000000000000..9a850e8bc20564 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts @@ -0,0 +1,126 @@ +/* + * 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 { useEffect, useRef, useState } from 'react'; +import { debounce } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types +// 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 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; + +interface FuncArgs { + fieldSelected: IFieldType | undefined; + patterns: IIndexPattern | undefined; + searchQuery: string; + value: string | string[] | undefined; +} + +type Func = (args: FuncArgs) => void; + +export type UseFieldValueAutocompleteReturn = [boolean, boolean, string[], Func | null]; + +export interface UseFieldValueAutocompleteProps { + autocompleteService: AutocompleteStart; + fieldValue: string | string[] | undefined; + indexPattern: IIndexPattern | undefined; + operatorType: OperatorTypeEnum; + query: string; + selectedField: IFieldType | undefined; +} +/** + * Hook for using the field value autocomplete service + */ +export const useFieldValueAutocomplete = ({ + selectedField, + operatorType, + fieldValue, + query, + indexPattern, + autocompleteService, +}: UseFieldValueAutocompleteProps): UseFieldValueAutocompleteReturn => { + 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, patterns, searchQuery }: 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 autocompleteService.getValueSuggestions({ + field, + indexPattern: patterns, + 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, + patterns: indexPattern, + searchQuery: query, + value: fieldValue, + }); + } + + updateSuggestions.current = fetchSuggestions; + + return (): void => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [selectedField, operatorType, fieldValue, indexPattern, query, autocompleteService]); + + return [isLoading, isSuggestingValues, suggestions, updateSuggestions.current]; +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/index.ts b/packages/kbn-securitysolution-autocomplete/src/index.ts new file mode 100644 index 00000000000000..69d6cfea35ac44 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/index.ts @@ -0,0 +1,12 @@ +/* + * 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 './get_generic_combo_box_props'; +export * from './hooks'; +export * from './param_is_valid'; diff --git a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.test.ts new file mode 100644 index 00000000000000..d944fb2fdccc54 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.test.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +describe('params_is_valid', () => { + test('Tests should be ported', () => { + // TODO: Port all the tests from: x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts here once mocks are figured out and kbn package mocks are figured out + expect(true).toBe(true); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts new file mode 100644 index 00000000000000..bd777356866a62 --- /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 +// 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/param_is_valid/translations.ts b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts new file mode 100644 index 00000000000000..ef1ee68fe282fa --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts @@ -0,0 +1,17 @@ +/* + * 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 NUMBER_ERR = i18n.translate('xpack.autocomplete.invalidNumberError', { + defaultMessage: 'Not a valid number', +}); + +export const DATE_ERR = i18n.translate('xpack.autocomplete.invalidDateError', { + defaultMessage: 'Not a valid date', +}); diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.json b/packages/kbn-securitysolution-autocomplete/tsconfig.json new file mode 100644 index 00000000000000..df165c7e42d194 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-autocomplete/src", + "types": ["jest", "node", "resize-observer-polyfill"] + }, + "include": ["src/**/*"] +} diff --git a/yarn.lock b/yarn.lock index dccbd8f91a4299..4293f6309b3053 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2847,6 +2847,10 @@ version "0.0.0" uid "" +"@kbn/securitysolution-autocomplete@link:bazel-bin/packages/kbn-securitysolution-autocomplete": + version "0.0.0" + uid "" + "@kbn/securitysolution-es-utils@link:bazel-bin/packages/kbn-securitysolution-es-utils": version "0.0.0" uid "" From efa6d3b1a405c5c16859ddf4dc6fd96295a42d67 Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Mon, 12 Jul 2021 21:34:37 -0600 Subject: [PATCH 2/8] Fixed i18n keys --- .../src/check_empty_value/translations.ts | 2 +- .../src/field_value_match/translations.ts | 11 ++++------- .../src/param_is_valid/translations.ts | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts index 44709743b3508d..bf0b43ef80ebb1 100644 --- a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts @@ -8,6 +8,6 @@ import { i18n } from '@kbn/i18n'; -export const FIELD_REQUIRED_ERR = i18n.translate('xpack.autocomplete.fieldRequiredError', { +export const FIELD_REQUIRED_ERR = i18n.translate('autocomplete.fieldRequiredError', { defaultMessage: 'Value cannot be empty', }); diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts b/packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts index 0ed97753bbca3e..7433a4489ca828 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts @@ -8,13 +8,10 @@ import { i18n } from '@kbn/i18n'; -export const LOADING = i18n.translate('xpack.autocomplete.loadingDescription', { +export const LOADING = i18n.translate('autocomplete.loadingDescription', { defaultMessage: 'Loading...', }); -export const SELECT_FIELD_FIRST = i18n.translate( - 'xpack.securitySolution.autocomplete.selectField', - { - defaultMessage: 'Please select a field first...', - } -); +export const SELECT_FIELD_FIRST = i18n.translate('autocomplete.selectField', { + defaultMessage: 'Please select a field first...', +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts index ef1ee68fe282fa..74bbbd6d16872e 100644 --- a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts +++ b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts @@ -8,10 +8,10 @@ import { i18n } from '@kbn/i18n'; -export const NUMBER_ERR = i18n.translate('xpack.autocomplete.invalidNumberError', { +export const NUMBER_ERR = i18n.translate('autocomplete.invalidNumberError', { defaultMessage: 'Not a valid number', }); -export const DATE_ERR = i18n.translate('xpack.autocomplete.invalidDateError', { +export const DATE_ERR = i18n.translate('autocomplete.invalidDateError', { defaultMessage: 'Not a valid date', }); From 57b65e01ac7f91caff6cbccccdca1182926a44da Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Wed, 14 Jul 2021 10:49:17 -0600 Subject: [PATCH 3/8] Copied a few other patterns to try and get ?? to work --- .../BUILD.bazel | 45 ++- .../babel.config.js | 19 ++ .../package.json | 5 +- .../react/package.json | 5 + .../src/field_value_match/index.tsx | 13 +- .../src/index.ts | 1 + .../tsconfig.browser.json | 23 ++ .../tsconfig.json | 8 +- .../autocomplete/field_value_match.test.tsx | 2 +- .../autocomplete/field_value_match.tsx | 305 ------------------ .../components/autocomplete/index.tsx | 1 - .../components/builder/entry_renderer.tsx | 2 +- 12 files changed, 100 insertions(+), 329 deletions(-) create mode 100644 packages/kbn-securitysolution-autocomplete/babel.config.js create mode 100644 packages/kbn-securitysolution-autocomplete/react/package.json create mode 100644 packages/kbn-securitysolution-autocomplete/tsconfig.browser.json delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel index e8d54b30f11a0d..cf7fca08cb9a08 100644 --- a/packages/kbn-securitysolution-autocomplete/BUILD.bazel +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -24,13 +24,18 @@ filegroup( ) 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", + "@npm//@babel/core", + "@npm//babel-loader", "@npm//@elastic/eui", "@npm//react", "@npm//resize-observer-polyfill", @@ -39,6 +44,7 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "@npm//typescript", "@npm//@types/jest", "@npm//@types/node", "@npm//@types/react", @@ -50,22 +56,47 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", ], ) ts_project( name = "tsc", - srcs = SRCS, args = ["--pretty"], + srcs = SRCS, + deps = DEPS, + allow_js = True, declaration = True, + declaration_dir = "target_types", declaration_map = True, incremental = True, - out_dir = "target", + 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( @@ -73,20 +104,20 @@ js_library( package_name = PKG_REQUIRE_NAME, srcs = NPM_MODULE_EXTRA_FILES, visibility = ["//visibility:public"], - deps = DEPS + [":tsc"], + deps = [":tsc", ":tsc_browser"] + DEPS, ) pkg_npm( name = "npm_module", deps = [ - ":%s" % PKG_BASE_NAME, - ], + ":%s" % PKG_BASE_NAME, + ] ) filegroup( name = "build", srcs = [ - ":npm_module", + ":npm_module", ], visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-autocomplete/babel.config.js b/packages/kbn-securitysolution-autocomplete/babel.config.js new file mode 100644 index 00000000000000..b4a118df51af51 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/babel.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + env: { + web: { + presets: ['@kbn/babel-preset/webpack_preset'], + }, + node: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + ignore: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/packages/kbn-securitysolution-autocomplete/package.json b/packages/kbn-securitysolution-autocomplete/package.json index 29d6edcec6bee3..06e1e7ab004607 100644 --- a/packages/kbn-securitysolution-autocomplete/package.json +++ b/packages/kbn-securitysolution-autocomplete/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "description": "Security Solution auto complete", "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "browser": "./target_web/browser.js", + "main": "./target_web/index.js", + "types": "./target_types/index.d.ts", "private": true } diff --git a/packages/kbn-securitysolution-autocomplete/react/package.json b/packages/kbn-securitysolution-autocomplete/react/package.json new file mode 100644 index 00000000000000..c5f222b5843acf --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/react/package.json @@ -0,0 +1,5 @@ +{ + "browser": "../target_web/react", + "main": "../target_node/react", + "types": "../target_types/react/index.d.ts" +} diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx index 7f4b91569128b9..0d7df7e351b585 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx @@ -195,11 +195,6 @@ export const AutocompleteFieldMatchComponent: React.FC (fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}), - [fieldInputWidth] - ); - useEffect((): void => { setError(undefined); if (onError != null) { @@ -231,7 +226,7 @@ export const AutocompleteFieldMatchComponent: React.FC @@ -240,7 +235,7 @@ export const AutocompleteFieldMatchComponent: React.FC @@ -295,7 +290,7 @@ export const AutocompleteFieldMatchComponent: React.FC diff --git a/packages/kbn-securitysolution-autocomplete/src/index.ts b/packages/kbn-securitysolution-autocomplete/src/index.ts index 69d6cfea35ac44..034a29f3d14b67 100644 --- a/packages/kbn-securitysolution-autocomplete/src/index.ts +++ b/packages/kbn-securitysolution-autocomplete/src/index.ts @@ -7,6 +7,7 @@ */ export * from './check_empty_value'; export * from './field'; +export * from './field_value_match'; export * from './get_generic_combo_box_props'; export * from './hooks'; export * from './param_is_valid'; diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json new file mode 100644 index 00000000000000..bab7b18c59cfde --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "outDir": "./target_web", + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-securitysolution-autocomplete/src", + "types": [ + "jest", + "node" + ], + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.json b/packages/kbn-securitysolution-autocomplete/tsconfig.json index df165c7e42d194..bf402e93ffd698 100644 --- a/packages/kbn-securitysolution-autocomplete/tsconfig.json +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.json @@ -1,13 +1,15 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "allowJs": true, + "incremental": true, + "declarationDir": "./target_types", + "outDir": "target_node", "declaration": true, "declarationMap": true, - "incremental": true, - "outDir": "target", - "rootDir": "src", "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/field_value_match.test.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx index c1ffb008e85635..6990e1aa05650f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx @@ -9,6 +9,7 @@ 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 '@kbn/securitysolution-autocomplete'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { @@ -16,7 +17,6 @@ import { 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'); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx deleted file mode 100644 index 8dbe8f223ae5bd..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx +++ /dev/null @@ -1,305 +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, useEffect, useMemo, useState } from 'react'; -import { - EuiComboBox, - EuiComboBoxOptionOption, - EuiFieldNumber, - EuiFormRow, - EuiSuperSelect, -} 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'; - -const BOOLEAN_OPTIONS = [ - { inputDisplay: 'true', value: 'true' }, - { inputDisplay: 'false', value: 'false' }, -]; - -const SINGLE_SELECTION = { asPlainText: true }; - -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; - autocompleteService: AutocompleteStart; - onChange: (arg: string) => void; - 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, - selectedField, - selectedValue, - indexPattern, - isLoading, - isDisabled = false, - isClearable = false, - isRequired = false, - fieldInputWidth, - onChange, - onError, - autocompleteService, -}): JSX.Element => { - const [searchQuery, setSearchQuery] = useState(''); - const [touched, setIsTouched] = useState(false); - const [error, setError] = useState(undefined); - const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({ - autocompleteService, - fieldValue: selectedValue, - indexPattern, - operatorType: OperatorTypeEnum.MATCH, - query: searchQuery, - selectedField, - }); - 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({ - getLabel, - options: optionsMemo, - selectedOptions: selectedOptionsMemo, - }), - [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); - return undefined; - } - }, - [isRequired, onChange, selectedField, touched, handleError] - ); - - const handleNonComboBoxInputChange = useCallback( - (event: React.ChangeEvent): void => { - const newValue = event.target.value; - onChange(newValue); - }, - [onChange] - ); - - const handleBooleanInputChange = useCallback( - (newOption: string): void => { - onChange(newOption); - }, - [onChange] - ); - - 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, - ]); - - const fieldInputWidths = useMemo( - () => (fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}), - [fieldInputWidth] - ); - - useEffect((): void => { - setError(undefined); - if (onError != null) { - onError(false); - } - }, [selectedField, onError]); - - const defaultInput = useMemo((): JSX.Element => { - return ( - - - - ); - }, [ - comboOptions, - error, - fieldInputWidths, - 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={fieldInputWidths} - fullWidth - /> - - ); - case 'boolean': - return ( - - - - ); - default: - return defaultInput; - } - } else { - return defaultInput; - } -}; - -AutocompleteFieldMatchComponent.displayName = 'AutocompleteFieldMatch'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx index 1623683f25ed55..0d351958e9396d 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx @@ -8,6 +8,5 @@ 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/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index c54da89766d76e..6734bfc0aa583c 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,6 +27,7 @@ import { getFilteredIndexPatterns, getOperatorOptions, } from '@kbn/securitysolution-list-utils'; +import { AutocompleteFieldMatchComponent } from '@kbn/securitysolution-autocomplete'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; @@ -34,7 +35,6 @@ 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'; From c08a3707992edd796a40562982678331e3d33506 Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Wed, 14 Jul 2021 14:32:42 -0600 Subject: [PATCH 4/8] Changed out the lists implementation for the autocomplete from packages --- .../src/get_generic_combo_box_props/index.ts | 10 +- .../components/autocomplete/field.test.tsx | 3 +- .../components/autocomplete/field.tsx | 153 ------------------ .../autocomplete/field_value_lists.tsx | 3 +- .../autocomplete/field_value_match_any.tsx | 2 +- .../components/autocomplete/helpers.test.ts | 14 +- .../components/autocomplete/helpers.ts | 114 ------------- .../components/autocomplete/index.tsx | 1 - .../components/autocomplete/operator.tsx | 3 +- .../components/builder/entry_renderer.tsx | 6 +- 10 files changed, 21 insertions(+), 288 deletions(-) delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx 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 index 5e42500039223e..0fba3c39344b80 100644 --- 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 @@ -20,15 +20,15 @@ export interface GetGenericComboBoxPropsReturn { * @param selectedOptions user selection if any * @param getLabel helper function to know which property to use for labels */ -export function getGenericComboBoxProps({ +export const getGenericComboBoxProps = ({ + getLabel, options, selectedOptions, - getLabel, }: { + getLabel: (value: T) => string; options: T[]; selectedOptions: T[]; - getLabel: (value: T) => string; -}): GetGenericComboBoxPropsReturn { +}): GetGenericComboBoxPropsReturn => { const newLabels = options.map(getLabel); const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); const newSelectedComboOptions = selectedOptions @@ -45,4 +45,4 @@ export function getGenericComboBoxProps({ labels: newLabels, selectedComboOptions: newSelectedComboOptions, }; -} +}; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx index 416852b469a79c..3c316193f583df 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx @@ -8,14 +8,13 @@ import React from 'react'; import { mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; 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( diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx deleted file mode 100644 index 47527914e71ff0..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx +++ /dev/null @@ -1,153 +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 } from 'react'; -import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; - -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; - -import { getGenericComboBoxProps } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; - -const AS_PLAIN_TEXT = { asPlainText: true }; - -interface OperatorProps { - fieldInputWidth?: number; - fieldTypeFilter?: string[]; - indexPattern: IIndexPattern | undefined; - isClearable: boolean; - isDisabled: boolean; - isLoading: boolean; - isRequired?: boolean; - onChange: (a: IFieldType[]) => void; - placeholder: string; - selectedField: IFieldType | undefined; -} - -/** - * There is a copy within: - * x-pack/plugins/security_solution/public/common/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 = ({ - fieldInputWidth, - fieldTypeFilter = [], - indexPattern, - isClearable = false, - isDisabled = false, - isLoading = false, - isRequired = false, - onChange, - placeholder, - selectedField, -}): 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]); - - const fieldWidth = useMemo(() => { - return fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}; - }, [fieldInputWidth]); - - 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({ - getLabel: (field) => field.name, - options: availableFields, - selectedOptions: selectedFields, - }); -}; - -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/lists/public/exceptions/components/autocomplete/field_value_lists.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx index 047f8ef33c8c0c..34f67f2d402164 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx @@ -10,10 +10,11 @@ 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 { getGenericComboBoxProps } from '@kbn/securitysolution-autocomplete'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { filterFieldToList, getGenericComboBoxProps } from './helpers'; +import { filterFieldToList } from './helpers'; import * as i18n from './translations'; const SINGLE_SELECTION = { asPlainText: true }; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx index e5a5e76f8cc5d6..c1820d584bc08a 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx @@ -9,12 +9,12 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { uniq } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { getGenericComboBoxProps, paramIsValid } from '@kbn/securitysolution-autocomplete'; 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'; 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 index 21764c6f459d87..585d0fc533a428 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts @@ -14,20 +14,18 @@ import { isNotOperator, isOperator, } from '@kbn/securitysolution-list-utils'; +import { + checkEmptyValue, + getGenericComboBoxProps, + paramIsValid, +} from '@kbn/securitysolution-autocomplete'; 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'; +import { filterFieldToList, getOperators, typeMatch } from './helpers'; describe('helpers', () => { // @ts-ignore diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts index 975416e272227c..fb8e2183ac0b51 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts @@ -5,8 +5,6 @@ * 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, @@ -19,9 +17,6 @@ import { import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - /** * Returns the appropriate operators given a field type * @@ -40,115 +35,6 @@ export const getOperators = (field: IFieldType | undefined): OperatorOption[] => } }; -/** - * 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 diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx index 0d351958e9396d..21478d14819047 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx @@ -8,5 +8,4 @@ export { AutocompleteFieldExistsComponent } from './field_value_exists'; export { AutocompleteFieldListsComponent } from './field_value_lists'; export { AutocompleteFieldMatchAnyComponent } from './field_value_match_any'; -export { FieldComponent } from './field'; export { OperatorComponent } from './operator'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx index 0d2fe5bd664be1..e55eb7fa46fdfc 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx @@ -8,10 +8,11 @@ import React, { useCallback, useMemo } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { OperatorOption } from '@kbn/securitysolution-list-utils'; +import { getGenericComboBoxProps } from '@kbn/securitysolution-autocomplete'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { getGenericComboBoxProps, getOperators } from './helpers'; +import { getOperators } from './helpers'; import { GetGenericComboBoxPropsReturn } from './types'; const AS_PLAIN_TEXT = { asPlainText: true }; 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 6734bfc0aa583c..7abdf4f88cf3d7 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,12 +27,14 @@ import { getFilteredIndexPatterns, getOperatorOptions, } from '@kbn/securitysolution-list-utils'; -import { AutocompleteFieldMatchComponent } from '@kbn/securitysolution-autocomplete'; +import { + AutocompleteFieldMatchComponent, + FieldComponent, +} 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 { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any'; From fb0a549702ff527b7fde3a6b61c452ba4cfe705a Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Wed, 14 Jul 2021 17:54:55 -0600 Subject: [PATCH 5/8] Integrated more with packages, some issues with mocks and jest tests --- .../package.json | 4 +- .../autocomplete/field_value_match.test.tsx | 16 +- .../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 +- 17 files changed, 21 insertions(+), 1977 deletions(-) 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/packages/kbn-securitysolution-autocomplete/package.json b/packages/kbn-securitysolution-autocomplete/package.json index 06e1e7ab004607..5cfd18b63256a9 100644 --- a/packages/kbn-securitysolution-autocomplete/package.json +++ b/packages/kbn-securitysolution-autocomplete/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "Security Solution auto complete", "license": "SSPL-1.0 OR Elastic License 2.0", - "browser": "./target_web/browser.js", - "main": "./target_web/index.js", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", "types": "./target_types/index.d.ts", "private": true } diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx index 6990e1aa05650f..ec34de26791ebf 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx @@ -9,7 +9,10 @@ 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 '@kbn/securitysolution-autocomplete'; +import { + AutocompleteFieldMatchComponent, + useFieldValueAutocomplete, +} from '@kbn/securitysolution-autocomplete'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { @@ -17,9 +20,8 @@ import { getField, } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -jest.mock('./hooks/use_field_value_autocomplete'); +// TODO: Mock this +// jest.mock('../hooks/use_field_value_autocomplete'); const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); @@ -201,7 +203,7 @@ describe('AutocompleteFieldMatchComponent', () => { expect(mockOnChange).toHaveBeenCalledWith('126.45.211.34'); }); - test('it invokes "onChange" when new value selected', async () => { + test.skip('it invokes "onChange" when new value selected', async () => { const mockOnChange = jest.fn(); wrapper = mount( { expect(mockOnChange).toHaveBeenCalledWith('value 1'); }); - test('it refreshes autocomplete with search query when new value searched', () => { + test.skip('it refreshes autocomplete with search query when new value searched', () => { wrapper = mount( { selectedValue="" /> ); - expect( wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').exists() ).toBeTruthy(); @@ -431,7 +432,6 @@ describe('AutocompleteFieldMatchComponent', () => { selectedValue="" /> ); - wrapper .find('[data-test-subj="valueAutocompleteFieldMatchNumber"] input') .at(0) diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx deleted file mode 100644 index 79e6fe5506b84a..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; - -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { FieldComponent } from './field'; - -describe('FieldComponent', () => { - test('it renders disabled if "isDisabled" is true', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] input`).prop('disabled') - ).toBeTruthy(); - }); - - test('it renders loading if "isLoading" is true', () => { - const wrapper = mount( - - ); - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] button`).at(0).simulate('click'); - expect( - wrapper - .find(`EuiComboBoxOptionsList[data-test-subj="fieldAutocompleteComboBox-optionsList"]`) - .prop('isLoading') - ).toBeTruthy(); - }); - - test('it allows user to clear values if "isClearable" is true', () => { - const wrapper = mount( - - ); - - expect( - wrapper - .find(`[data-test-subj="comboBoxInput"]`) - .hasClass('euiComboBox__inputWrap-isClearable') - ).toBeTruthy(); - }); - - test('it correctly displays selected field', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] EuiComboBoxPill`).at(0).text() - ).toEqual('machine.os.raw'); - }); - - test('it invokes "onChange" when option selected', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ label: 'machine.os' }]); - - expect(mockOnChange).toHaveBeenCalledWith([ - { - aggregatable: true, - count: 0, - esTypes: ['text'], - name: 'machine.os', - readFromDocValues: false, - scripted: false, - searchable: true, - type: 'string', - }, - ]); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx deleted file mode 100644 index a175a9b847c715..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useMemo, useCallback } from 'react'; -import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; - -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { getGenericComboBoxProps } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; - -interface OperatorProps { - placeholder: string; - selectedField: IFieldType | undefined; - indexPattern: IIndexPattern | undefined; - isLoading: boolean; - isDisabled: boolean; - isClearable: boolean; - fieldTypeFilter?: string[]; - fieldInputWidth?: number; - isRequired?: boolean; - onChange: (a: IFieldType[]) => void; -} - -/** - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * NOTE: This has deviated from the copy and will have to be reconciled. - */ -export const FieldComponent: React.FC = ({ - placeholder, - selectedField, - indexPattern, - isLoading = false, - isDisabled = false, - isClearable = false, - isRequired = false, - fieldTypeFilter = [], - fieldInputWidth, - onChange, -}): JSX.Element => { - const [touched, setIsTouched] = useState(false); - - const { availableFields, selectedFields } = useMemo( - () => getComboBoxFields(indexPattern, selectedField, fieldTypeFilter), - [indexPattern, selectedField, fieldTypeFilter] - ); - - const { comboOptions, labels, selectedComboOptions } = useMemo( - () => getComboBoxProps({ availableFields, selectedFields }), - [availableFields, selectedFields] - ); - - const handleValuesChange = useCallback( - (newOptions: EuiComboBoxOptionOption[]): void => { - const newValues: IFieldType[] = newOptions.map( - ({ label }) => availableFields[labels.indexOf(label)] - ); - onChange(newValues); - }, - [availableFields, labels, onChange] - ); - - const handleTouch = useCallback((): void => { - setIsTouched(true); - }, [setIsTouched]); - - return ( - - ); -}; - -FieldComponent.displayName = 'Field'; - -interface ComboBoxFields { - availableFields: IFieldType[]; - selectedFields: IFieldType[]; -} - -const getComboBoxFields = ( - indexPattern: IIndexPattern | undefined, - selectedField: IFieldType | undefined, - fieldTypeFilter: string[] -): ComboBoxFields => { - const existingFields = getExistingFields(indexPattern); - const selectedFields = getSelectedFields(selectedField); - const availableFields = getAvailableFields(existingFields, selectedFields, fieldTypeFilter); - - return { availableFields, selectedFields }; -}; - -const getComboBoxProps = (fields: ComboBoxFields): GetGenericComboBoxPropsReturn => { - const { availableFields, selectedFields } = fields; - - return getGenericComboBoxProps({ - options: availableFields, - selectedOptions: selectedFields, - getLabel: (field) => field.name, - }); -}; - -const getExistingFields = (indexPattern: IIndexPattern | undefined): IFieldType[] => { - return indexPattern != null ? indexPattern.fields : []; -}; - -const getSelectedFields = (selectedField: IFieldType | undefined): IFieldType[] => { - return selectedField ? [selectedField] : []; -}; - -const getAvailableFields = ( - existingFields: IFieldType[], - selectedFields: IFieldType[], - fieldTypeFilter: string[] -): IFieldType[] => { - const fieldsByName = new Map(); - - existingFields.forEach((f) => fieldsByName.set(f.name, f)); - selectedFields.forEach((f) => fieldsByName.set(f.name, f)); - - const uniqueFields = Array.from(fieldsByName.values()); - - if (fieldTypeFilter.length > 0) { - return uniqueFields.filter(({ type }) => fieldTypeFilter.includes(type)); - } - - return uniqueFields; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx deleted file mode 100644 index 38d103fe651301..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; -import { EuiSuperSelect, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { act } from '@testing-library/react'; - -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { AutocompleteFieldMatchComponent } from './field_value_match'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -jest.mock('./hooks/use_field_value_autocomplete'); - -describe('AutocompleteFieldMatchComponent', () => { - let wrapper: ReactWrapper; - - const getValueSuggestionsMock = jest - .fn() - .mockResolvedValue([false, true, ['value 3', 'value 4'], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - true, - ['value 1', 'value 2'], - getValueSuggestionsMock, - ]); - }); - - afterEach(() => { - jest.clearAllMocks(); - wrapper.unmount(); - }); - - test('it renders row label if one passed in', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchLabel"] label').at(0).text() - ).toEqual('Row Label'); - }); - - test('it renders disabled if "isDisabled" is true', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] input').prop('disabled') - ).toBeTruthy(); - }); - - test('it renders loading if "isLoading" is true', () => { - wrapper = mount( - - ); - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] button').at(0).simulate('click'); - expect( - wrapper - .find('EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteMatch-optionsList"]') - .prop('isLoading') - ).toBeTruthy(); - }); - - test('it allows user to clear values if "isClearable" is true', () => { - wrapper = mount( - - ); - - expect( - wrapper - .find('[data-test-subj="comboBoxInput"]') - .hasClass('euiComboBox__inputWrap-isClearable') - ).toBeTruthy(); - }); - - test('it correctly displays selected value', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] EuiComboBoxPill').at(0).text() - ).toEqual('126.45.211.34'); - }); - - test('it invokes "onChange" when new value created', async () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onCreateOption: (a: string) => void; - }).onCreateOption('126.45.211.34'); - - expect(mockOnChange).toHaveBeenCalledWith('126.45.211.34'); - }); - - test('it invokes "onChange" when new value selected', async () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ label: 'value 1' }]); - - expect(mockOnChange).toHaveBeenCalledWith('value 1'); - }); - - test('it refreshes autocomplete with search query when new value searched', () => { - wrapper = mount( - - ); - act(() => { - ((wrapper.find(EuiComboBox).props() as unknown) as { - onSearchChange: (a: string) => void; - }).onSearchChange('value 1'); - }); - - expect(useFieldValueAutocomplete).toHaveBeenCalledWith({ - selectedField: getField('machine.os.raw'), - operatorType: 'match', - query: 'value 1', - fieldValue: '', - indexPattern: { - id: '1234', - title: 'logstash-*', - fields, - }, - }); - }); - - describe('boolean type', () => { - const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - false, - [], - valueSuggestionsMock, - ]); - }); - - test('it displays only two options - "true" or "false"', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').exists() - ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').at(0).prop('options') - ).toEqual([ - { - inputDisplay: 'true', - value: 'true', - }, - { - inputDisplay: 'false', - value: 'false', - }, - ]); - }); - - test('it invokes "onChange" with "true" when selected', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiSuperSelect).props() as unknown) as { - onChange: (a: string) => void; - }).onChange('true'); - - expect(mockOnChange).toHaveBeenCalledWith('true'); - }); - - test('it invokes "onChange" with "false" when selected', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiSuperSelect).props() as unknown) as { - onChange: (a: string) => void; - }).onChange('false'); - - expect(mockOnChange).toHaveBeenCalledWith('false'); - }); - }); - - describe('number type', () => { - const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - false, - [], - valueSuggestionsMock, - ]); - }); - - test('it number input when field type is number', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valueAutocompleteFieldMatchNumber"]').exists() - ).toBeTruthy(); - }); - - test('it invokes "onChange" with numeric value when inputted', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - wrapper - .find('[data-test-subj="valueAutocompleteFieldMatchNumber"] input') - .at(0) - .simulate('change', { target: { value: '8' } }); - - expect(mockOnChange).toHaveBeenCalledWith('8'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx deleted file mode 100644 index 21d1d9b4b31aa1..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useMemo, useState, useEffect } from 'react'; -import { - EuiSuperSelect, - EuiFormRow, - EuiFieldNumber, - EuiComboBoxOptionOption, - EuiComboBox, -} from '@elastic/eui'; -import { uniq } from 'lodash'; - -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; -import { paramIsValid, getGenericComboBoxProps } from './helpers'; - -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - -interface AutocompleteFieldMatchProps { - placeholder: string; - selectedField: IFieldType | undefined; - selectedValue: string | undefined; - indexPattern: IIndexPattern | undefined; - isLoading: boolean; - isDisabled: boolean; - isClearable: boolean; - isRequired?: boolean; - fieldInputWidth?: number; - rowLabel?: string; - onChange: (arg: string) => void; - onError?: (arg: boolean) => void; -} - -/** - * There is a copy of this within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const AutocompleteFieldMatchComponent: React.FC = ({ - placeholder, - rowLabel, - selectedField, - selectedValue, - indexPattern, - isLoading, - isDisabled = false, - isClearable = false, - isRequired = false, - fieldInputWidth, - onChange, - onError, -}): JSX.Element => { - const [searchQuery, setSearchQuery] = useState(''); - const [touched, setIsTouched] = useState(false); - const [error, setError] = useState(undefined); - const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({ - selectedField, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: selectedValue, - query: searchQuery, - indexPattern, - }); - const getLabel = useCallback((option: string): string => option, []); - const optionsMemo = useMemo((): string[] => { - const valueAsStr = String(selectedValue); - return selectedValue != null && selectedValue.trim() !== '' - ? uniq([valueAsStr, ...suggestions]) - : suggestions; - }, [suggestions, selectedValue]); - const selectedOptionsMemo = useMemo((): string[] => { - const valueAsStr = String(selectedValue); - return selectedValue ? [valueAsStr] : []; - }, [selectedValue]); - - const handleError = useCallback( - (err: string | undefined): void => { - setError((existingErr): string | undefined => { - const oldErr = existingErr != null; - const newErr = err != null; - if (oldErr !== newErr && onError != null) { - onError(newErr); - } - - return err; - }); - }, - [setError, onError] - ); - - const { comboOptions, labels, selectedComboOptions } = useMemo( - (): GetGenericComboBoxPropsReturn => - getGenericComboBoxProps({ - options: optionsMemo, - selectedOptions: selectedOptionsMemo, - getLabel, - }), - [optionsMemo, selectedOptionsMemo, getLabel] - ); - - const handleValuesChange = useCallback( - (newOptions: EuiComboBoxOptionOption[]): void => { - const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]); - handleError(undefined); - onChange(newValue ?? ''); - }, - [handleError, labels, onChange, optionsMemo] - ); - - const handleSearchChange = useCallback( - (searchVal: string): void => { - if (searchVal !== '' && selectedField != null) { - const err = paramIsValid(searchVal, selectedField, isRequired, touched); - handleError(err); - - setSearchQuery(searchVal); - } - }, - [handleError, isRequired, selectedField, touched] - ); - - const handleCreateOption = useCallback( - (option: string): boolean | undefined => { - const err = paramIsValid(option, selectedField, isRequired, touched); - handleError(err); - - if (err != null) { - // Explicitly reject the user's input - return false; - } else { - onChange(option); - } - }, - [isRequired, onChange, selectedField, touched, handleError] - ); - - const handleNonComboBoxInputChange = (event: React.ChangeEvent): void => { - const newValue = event.target.value; - onChange(newValue); - }; - - const handleBooleanInputChange = (newOption: string): void => { - onChange(newOption); - }; - - const setIsTouchedValue = useCallback((): void => { - setIsTouched(true); - - const err = paramIsValid(selectedValue, selectedField, isRequired, true); - handleError(err); - }, [setIsTouched, handleError, selectedValue, selectedField, isRequired]); - - const inputPlaceholder = useMemo((): string => { - if (isLoading || isLoadingSuggestions) { - return i18n.LOADING; - } else if (selectedField == null) { - return i18n.SELECT_FIELD_FIRST; - } else { - return placeholder; - } - }, [isLoading, selectedField, isLoadingSuggestions, placeholder]); - - const isLoadingState = useMemo((): boolean => isLoading || isLoadingSuggestions, [ - isLoading, - isLoadingSuggestions, - ]); - - useEffect((): void => { - setError(undefined); - if (onError != null) { - onError(false); - } - }, [selectedField, onError]); - - const defaultInput = useMemo((): JSX.Element => { - return ( - - - - ); - }, [ - comboOptions, - error, - fieldInputWidth, - handleCreateOption, - handleSearchChange, - handleValuesChange, - inputPlaceholder, - isClearable, - isDisabled, - isLoadingState, - rowLabel, - selectedComboOptions, - selectedField, - setIsTouchedValue, - ]); - - if (!isSuggestingValues && selectedField != null) { - switch (selectedField.type) { - case 'number': - return ( - - 0 - ? parseFloat(selectedValue) - : selectedValue ?? '' - } - onChange={handleNonComboBoxInputChange} - data-test-subj="valueAutocompleteFieldMatchNumber" - style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}} - fullWidth - /> - - ); - case 'boolean': - return ( - - - - ); - default: - return defaultInput; - } - } else { - return defaultInput; - } -}; - -AutocompleteFieldMatchComponent.displayName = 'AutocompleteFieldMatch'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts deleted file mode 100644 index 1618de245365dc..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import '../../../common/mock/match_media'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -import * as i18n from './translations'; -import { checkEmptyValue, paramIsValid, getGenericComboBoxProps } from './helpers'; - -describe('helpers', () => { - // @ts-ignore - moment.suppressDeprecationWarnings = true; - - describe('#checkEmptyValue', () => { - test('returns no errors if no field has been selected', () => { - const isValid = checkEmptyValue('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = checkEmptyValue('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns null if input value is not empty string or undefined', () => { - const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true); - - expect(isValid).toBeNull(); - }); - }); - - describe('#paramIsValid', () => { - test('returns no errors if no field has been selected', () => { - const isValid = paramIsValid('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = paramIsValid('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type date and value is valid', () => { - const isValid = paramIsValid( - '1994-11-05T08:15:30-05:00', - getField('@timestamp'), - false, - true - ); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if filed is of type date and value is not valid', () => { - const isValid = paramIsValid('1593478826', getField('@timestamp'), false, true); - - expect(isValid).toEqual(i18n.DATE_ERR); - }); - - test('returns no errors if field is of type number and value is an integer', () => { - const isValid = paramIsValid('4', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a float', () => { - const isValid = paramIsValid('4.3', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a long', () => { - const isValid = paramIsValid('-9223372036854775808', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if field is of type number and value is "hello"', () => { - const isValid = paramIsValid('hello', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - - test('returns errors if field is of type number and value is "123abc"', () => { - const isValid = paramIsValid('123abc', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - }); - - describe('#getGenericComboBoxProps', () => { - test('it returns empty arrays if "options" is empty array', () => { - const result = getGenericComboBoxProps({ - options: [], - selectedOptions: ['option1'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); - }); - - test('it returns formatted props if "options" array is not empty', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: [], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it does not return "selectedOptions" items that do not appear in "options"', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option4'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it return "selectedOptions" items that do appear in "options"', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option2'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [ - { - label: 'option2', - }, - ], - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts deleted file mode 100644 index 890f1e67558349..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import dateMath from '@elastic/datemath'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; - -import { IFieldType } from '../../../../../../../src/plugins/data/common'; - -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - -/** - * Determines if empty value is ok - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const checkEmptyValue = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined | null => { - if (isRequired && touched && (param == null || param.trim() === '')) { - return i18n.FIELD_REQUIRED_ERR; - } - - if ( - field == null || - (isRequired && !touched) || - (!isRequired && (param == null || param === '')) - ) { - return undefined; - } - - return null; -}; - -/** - * Very basic validation for values - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * @param param the value being checked - * @param field the selected field - * @param isRequired whether or not an empty value is allowed - * @param touched has field been touched by user - * @returns undefined if valid, string with error message if invalid - */ -export const paramIsValid = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined => { - if (field == null) { - return undefined; - } - - const emptyValueError = checkEmptyValue(param, field, isRequired, touched); - if (emptyValueError !== null) { - return emptyValueError; - } - - switch (field.type) { - case 'date': - const moment = dateMath.parse(param ?? ''); - const isDate = Boolean(moment && moment.isValid()); - return isDate ? undefined : i18n.DATE_ERR; - case 'number': - const isNum = param != null && param.trim() !== '' && !isNaN(+param); - return isNum ? undefined : i18n.NUMBER_ERR; - default: - return undefined; - } -}; - -/** - * Determines the options, selected values and option labels for EUI combo box - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * @param options options user can select from - * @param selectedOptions user selection if any - * @param getLabel helper function to know which property to use for labels - */ -export function getGenericComboBoxProps({ - options, - selectedOptions, - getLabel, -}: { - options: T[]; - selectedOptions: T[]; - getLabel: (value: T) => string; -}): GetGenericComboBoxPropsReturn { - const newLabels = options.map(getLabel); - const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); - const newSelectedComboOptions = selectedOptions - .map(getLabel) - .filter((option) => { - return newLabels.indexOf(option) !== -1; - }) - .map((option) => { - return newComboOptions[newLabels.indexOf(option)]; - }); - - return { - comboOptions: newComboOptions, - labels: newLabels, - selectedComboOptions: newSelectedComboOptions, - }; -} diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts deleted file mode 100644 index e0bdbf2603dc32..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act, renderHook } from '@testing-library/react-hooks'; - -import { - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn, - useFieldValueAutocomplete, -} from './use_field_value_autocomplete'; -import { useKibana } from '../../../../common/lib/kibana'; -import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; -import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; - -jest.mock('../../../../common/lib/kibana'); - -describe('useFieldValueAutocomplete', () => { - const onErrorMock = jest.fn(); - const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); - - beforeEach(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: getValueSuggestionsMock, - }, - }, - }, - }); - }); - - afterEach(() => { - onErrorMock.mockClear(); - getValueSuggestionsMock.mockClear(); - }); - - test('initializes hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: undefined, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: undefined, - query: '', - }) - ); - await waitForNextUpdate(); - - expect(result.current).toEqual([false, true, [], result.current[3]]); - }); - }); - - test('does not call autocomplete service if "operatorType" is "exists"', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('machine.os'), - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('does not call autocomplete service if "selectedField" is undefined', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: undefined, - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('does not call autocomplete service if "indexPattern" is undefined', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('machine.os'), - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: undefined, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('it uses full path name for nested fields to fetch suggestions', async () => { - const suggestionsMock = jest.fn().mockResolvedValue([]); - - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: suggestionsMock, - }, - }, - }, - }); - await act(async () => { - const signal = new AbortController().signal; - const { waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: { ...getField('nestedField.child'), name: 'child' }, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(suggestionsMock).toHaveBeenCalledWith({ - field: { ...getField('nestedField.child'), name: 'nestedField.child' }, - indexPattern: { - fields: [ - { - aggregatable: true, - esTypes: ['integer'], - filterable: true, - name: 'response', - searchable: true, - type: 'number', - }, - ], - id: '1234', - title: 'logstash-*', - }, - query: '', - signal, - }); - }); - }); - - test('returns "isSuggestingValues" of false if field type is boolean', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('ssl'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, false, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns "isSuggestingValues" of false to note that autocomplete service is not in use if no autocomplete suggestions available', async () => { - const suggestionsMock = jest.fn().mockResolvedValue([]); - - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: suggestionsMock, - }, - }, - }, - }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('bytes'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, false, [], result.current[3]]; - - expect(suggestionsMock).toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns suggestions', async () => { - await act(async () => { - const signal = new AbortController().signal; - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('@tags'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [ - false, - true, - ['value 1', 'value 2'], - result.current[3], - ]; - - expect(getValueSuggestionsMock).toHaveBeenCalledWith({ - field: getField('@tags'), - indexPattern: stubIndexPatternWithFields, - query: '', - signal, - }); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns new suggestions on subsequent calls', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('@tags'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current[3]).not.toBeNull(); - - // Added check for typescripts sake, if null, - // would not reach below logic as test would stop above - if (result.current[3] != null) { - result.current[3]({ - fieldSelected: getField('@tags'), - value: 'hello', - patterns: stubIndexPatternWithFields, - searchQuery: '', - }); - } - - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [ - false, - true, - ['value 1', 'value 2'], - result.current[3], - ]; - - expect(getValueSuggestionsMock).toHaveBeenCalledTimes(2); - expect(result.current).toEqual(expectedResult); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts deleted file mode 100644 index 0fc4a663b7e11b..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState, useRef } from 'react'; -import { debounce } from 'lodash'; - -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { useKibana } from '../../../../common/lib/kibana'; - -interface FuncArgs { - fieldSelected: IFieldType | undefined; - value: string | string[] | undefined; - searchQuery: string; - patterns: IIndexPattern | undefined; -} - -type Func = (args: FuncArgs) => void; - -export type UseFieldValueAutocompleteReturn = [boolean, boolean, string[], Func | null]; - -export interface UseFieldValueAutocompleteProps { - selectedField: IFieldType | undefined; - operatorType: OperatorTypeEnum; - fieldValue: string | string[] | undefined; - query: string; - indexPattern: IIndexPattern | undefined; -} - -/** - * Hook for using the field value autocomplete service - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const useFieldValueAutocomplete = ({ - selectedField, - operatorType, - fieldValue, - query, - indexPattern, -}: UseFieldValueAutocompleteProps): UseFieldValueAutocompleteReturn => { - const { services } = useKibana(); - const [isLoading, setIsLoading] = useState(false); - const [isSuggestingValues, setIsSuggestingValues] = useState(true); - const [suggestions, setSuggestions] = useState([]); - const updateSuggestions = useRef(null); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchSuggestions = debounce( - async ({ fieldSelected, value, searchQuery, patterns }: FuncArgs) => { - try { - if (isSubscribed) { - if (fieldSelected == null || patterns == null) { - return; - } - - if (fieldSelected.type === 'boolean') { - setIsSuggestingValues(false); - return; - } - - setIsLoading(true); - - const field = - fieldSelected.subType != null && fieldSelected.subType.nested != null - ? { - ...fieldSelected, - name: `${fieldSelected.subType.nested.path}.${fieldSelected.name}`, - } - : fieldSelected; - - const newSuggestions = await services.data.autocomplete.getValueSuggestions({ - indexPattern: patterns, - field, - query: searchQuery, - signal: abortCtrl.signal, - }); - - if (newSuggestions.length === 0) { - setIsSuggestingValues(false); - } - - setIsLoading(false); - setSuggestions([...newSuggestions]); - } - } catch (error) { - if (isSubscribed) { - setSuggestions([]); - setIsLoading(false); - } - } - }, - 500 - ); - - if (operatorType !== OperatorTypeEnum.EXISTS) { - fetchSuggestions({ - fieldSelected: selectedField, - value: fieldValue, - searchQuery: query, - patterns: indexPattern, - }); - } - - updateSuggestions.current = fetchSuggestions; - - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [services.data.autocomplete, selectedField, operatorType, fieldValue, indexPattern, query]); - - return [isLoading, isSuggestingValues, suggestions, updateSuggestions.current]; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md b/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md deleted file mode 100644 index 2bf1867c008d23..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md +++ /dev/null @@ -1,122 +0,0 @@ -# Autocomplete Fields - -Need an input that shows available index fields? Or an input that autocompletes based on a selected indexPattern field? Bingo! That's what these components are for. They are generalized enough so that they can be reused throughout and repurposed based on your needs. - -All three of the available components rely on Eui's combo box. - -## useFieldValueAutocomplete - -This hook uses the kibana `services.data.autocomplete.getValueSuggestions()` service to return possible autocomplete fields based on the passed in `indexPattern` and `selectedField`. - -## FieldComponent - -This component can be used to display available indexPattern fields. It requires an indexPattern to be passed in and will show an error state if value is not one of the available indexPattern fields. Users will be able to select only one option. - -The `onChange` handler is passed `IFieldType[]`. - -```js - -``` - -## OperatorComponent - -This component can be used to display available operators. If you want to pass in your own operators, you can use `operatorOptions` prop. If a `operatorOptions` is provided, those will be used and it will ignore any of the built in logic that determines which operators to show. The operators within `operatorOptions` will still need to be of type `OperatorOption`. - -If no `operatorOptions` is provided, then the following behavior is observed: - -- if `selectedField` type is `boolean`, only `is`, `is not`, `exists`, `does not exist` operators will show -- if `selectedField` type is `nested`, only `is` operator will show -- if not one of the above, all operators will show (see `operators.ts`) - -The `onChange` handler is passed `OperatorOption[]`. - -```js - -``` - -## AutocompleteFieldExistsComponent - -This field value component is used when the selected operator is `exists` or `does not exist`. When these operators are selected, they are equivalent to using a wildcard. The combo box will be displayed as disabled. - -```js - -``` - -## AutocompleteFieldListsComponent - -This component can be used to display available large value lists - when operator selected is `is in list` or `is not in list`. It relies on hooks from the `lists` plugin. Users can only select one list and an error is shown if value is not one of available lists. - -The `selectedValue` should be the `id` of the selected list. - -This component relies on `selectedField` to render available lists. The reason being that it relies on the `selectedField` type to determine which lists to show as each large value list has a type as well. So if a user selects a field of type `ip`, it will only display lists of type `ip`. - -The `onChange` handler is passed `ListSchema`. - -```js - -``` - -## AutocompleteFieldMatchComponent - -This component can be used to allow users to select one single value. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own value. - -It does some minor validation, assuring that field value is a date if `selectedField` type is `date`, a number if `selectedField` type is `number`, an ip if `selectedField` type is `ip`. - -The `onChange` handler is passed selected `string`. - -```js - -``` - -## AutocompleteFieldMatchAnyComponent - -This component can be used to allow users to select multiple values. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own values. - -It does some minor validation, assuring that field values are a date if `selectedField` type is `date`, numbers if `selectedField` type is `number`, ips if `selectedField` type is `ip`. - -The `onChange` handler is passed selected `string[]`. - -```js - -``` diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts deleted file mode 100644 index 084f4b0698aacd..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const LOADING = i18n.translate('xpack.securitySolution.autocomplete.loadingDescription', { - defaultMessage: 'Loading...', -}); - -export const SELECT_FIELD_FIRST = i18n.translate( - 'xpack.securitySolution.autocomplete.selectField', - { - defaultMessage: 'Please select a field first...', - } -); - -export const FIELD_REQUIRED_ERR = i18n.translate( - 'xpack.securitySolution.autocomplete.fieldRequiredError', - { - defaultMessage: 'Value cannot be empty', - } -); - -export const NUMBER_ERR = i18n.translate('xpack.securitySolution.autocomplete.invalidNumberError', { - defaultMessage: 'Not a valid number', -}); - -export const DATE_ERR = i18n.translate('xpack.securitySolution.autocomplete.invalidDateError', { - defaultMessage: 'Not a valid date', -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts deleted file mode 100644 index 07f1903fb70e1c..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiComboBoxOptionOption } from '@elastic/eui'; - -export interface GetGenericComboBoxPropsReturn { - comboOptions: EuiComboBoxOptionOption[]; - labels: string[]; - selectedComboOptions: EuiComboBoxOptionOption[]; -} diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx index 49cfd841b7f8a6..49bd7824d61008 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx @@ -9,8 +9,8 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import { IFieldType, IndexPattern } from '../../../../../../../src/plugins/data/common'; -import { FieldComponent } from '../autocomplete/field'; import { FormattedEntry, Entry } from './types'; import * as i18n from './translations'; import { getEntryOnFieldChange, getEntryOnThreatFieldChange } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx index 503a568f137442..d74b1d0502acf5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx @@ -7,8 +7,8 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFormRow } from '@elastic/eui'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index c02f7992a9b926..eef18a502c2704 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -20,10 +20,10 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import { RiskScoreMapping } from '@kbn/securitysolution-io-ts-alerting-types'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 8b8c9441e7eae7..d4fbdc31fbcae9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -24,6 +24,11 @@ import { SeverityMapping, SeverityMappingItem, } from '@kbn/securitysolution-io-ts-alerting-types'; +import { + FieldComponent, + AutocompleteFieldMatchComponent, +} from '@kbn/securitysolution-autocomplete'; + import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; @@ -32,8 +37,7 @@ import { IFieldType, IIndexPattern, } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; -import { AutocompleteFieldMatchComponent } from '../../../../common/components/autocomplete/field_value_match'; +import { useKibana } from '../../../../common/lib/kibana'; const NestedContent = styled.div` margin-left: 24px; @@ -68,6 +72,7 @@ export const SeverityField = ({ isDisabled, options, }: SeverityFieldProps) => { + const { services } = useKibana(); const { value, isMappingChecked, mapping } = field.value; const { setValue } = field; @@ -254,6 +259,7 @@ export const SeverityField = ({ Date: Tue, 20 Jul 2021 12:37:26 -0600 Subject: [PATCH 6/8] Moves the rest of the files and tests into the kbn-package and adds Kibana tickets to all the TODO blocks --- .../BUILD.bazel | 2 + .../src/autocomplete/index.mock.ts | 15 + .../src/check_empty_value/index.test.ts | 43 +- .../src/check_empty_value/index.ts | 8 +- .../src/check_empty_value/translations.ts | 13 - .../src/field/index.test.tsx | 135 +++++- .../src/field/index.tsx | 2 +- .../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 | 21 +- .../src/field_value_match/index.test.tsx | 429 ++++++++++++++++- .../src/field_value_match/index.tsx | 6 +- .../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 ++ .../src/get_operators/index.test.ts | 53 +++ .../src/get_operators/index.ts | 38 ++ .../index.test.ts | 338 ++++++++++++- .../use_field_value_autocomplete/index.ts | 4 +- .../src/index.ts | 6 + .../src/list_schema/index.mock.ts | 51 ++ .../src/operator/index.test.tsx | 12 +- .../src/operator/index.tsx | 17 +- .../src/param_is_valid/index.test.ts | 94 +++- .../src/param_is_valid/index.ts | 4 +- .../src/param_is_valid/translations.ts | 17 - .../translations.ts => translations/index.ts} | 12 + .../src/type_match/index.test.ts | 59 +++ .../src/type_match/index.ts | 27 ++ .../components/autocomplete/README.md | 122 ----- .../components/autocomplete/field.test.tsx | 146 ------ .../autocomplete/field_value_match.test.tsx | 443 ------------------ .../components/autocomplete/helpers.test.ts | 386 --------------- .../components/autocomplete/helpers.ts | 69 --- .../use_field_value_autocomplete.test.ts | 334 ------------- .../hooks/use_field_value_autocomplete.ts | 123 ----- .../components/autocomplete/index.tsx | 11 - .../components/autocomplete/translations.ts | 28 -- .../components/autocomplete/types.ts | 14 - .../components/builder/entry_renderer.tsx | 8 +- 43 files changed, 1808 insertions(+), 1799 deletions(-) create mode 100644 packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts delete mode 100644 packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts 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_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_operators/index.test.ts create mode 100644 packages/kbn-securitysolution-autocomplete/src/get_operators/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%) delete mode 100644 packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts rename packages/kbn-securitysolution-autocomplete/src/{field_value_match/translations.ts => translations/index.ts} (63%) 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 delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/README.md delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx 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/hooks/use_field_value_autocomplete.test.ts delete mode 100644 x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.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 diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel index cf7fca08cb9a08..8e403a215d81d8 100644 --- a/packages/kbn-securitysolution-autocomplete/BUILD.bazel +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -13,6 +13,7 @@ SOURCE_FILES = glob( exclude = [ "**/*.test.*", "**/*.mock.*", + "**/*.mocks.*", ], ) @@ -34,6 +35,7 @@ SRC_DEPS = [ "//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", diff --git a/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts new file mode 100644 index 00000000000000..444a033b4887bb --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Copied from "src/plugins/data/public/mocks.ts" but without any type information +// TODO: Remove this in favor of the data/public/mocks if/when they become available, https://github.com/elastic/kibana/issues/100715 +export const autocompleteStartMock = { + getQuerySuggestions: jest.fn(), + getValueSuggestions: jest.fn(), + hasQuerySuggestions: jest.fn(), +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts index 417c2cc952e7a2..c36184e5c5ba11 100644 --- a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts @@ -6,9 +6,44 @@ * Side Public License, v 1. */ -describe('use_field_value_autocomplete', () => { - test('Tests should be ported', () => { - // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts here once mocks are figured out and kbn package mocks are figured out - expect(true).toBe(true); +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 index 1ce1967d4c4e84..894f233f73a5a1 100644 --- a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts @@ -6,18 +6,14 @@ * Side Public License, v 1. */ -import * as i18n from './translations'; +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 +// 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 - * 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, diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts deleted file mode 100644 index bf0b43ef80ebb1..00000000000000 --- a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/translations.ts +++ /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 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 FIELD_REQUIRED_ERR = i18n.translate('autocomplete.fieldRequiredError', { - defaultMessage: 'Value cannot be empty', -}); diff --git a/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx index c4255823f17d1f..08f55cef89b660 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx @@ -6,9 +6,138 @@ * 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'; + describe('field', () => { - test('Tests should be ported', () => { - // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx here once mocks are figured out and kbn package mocks are figured out - expect(true).toBe(true); + 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/packages/kbn-securitysolution-autocomplete/src/field/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx index 4c2e19a0afb24f..43342079ef92b5 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -// TODO: I have to use any here for now, but once this is available below, we should use the correct types +// 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; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx similarity index 70% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx index b6300581f12dd8..c4c07aff909e42 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx @@ -1,14 +1,15 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { mount } from 'enzyme'; -import { AutocompleteFieldExistsComponent } from './field_value_exists'; +import { AutocompleteFieldExistsComponent } from '.'; describe('AutocompleteFieldExistsComponent', () => { test('it renders field disabled', () => { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx similarity index 83% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx index ff70204e534838..37a16406e65a33 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx similarity index 88% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx index a338ce6a27d6c4..6fcf8ddf74b031 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -11,15 +12,20 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { waitFor } from '@testing-library/react'; import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; -import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; -import { DATE_NOW, IMMUTABLE, VERSION } from '../../../../../lists/common/constants.mock'; - -import { AutocompleteFieldListsComponent } from './field_value_lists'; - -const mockKibanaHttpService = coreMock.createStart().http; +import { getField } from '../fields/index.mock'; +import { AutocompleteFieldListsComponent } from '.'; +import { + getListResponseMock, + getFoundListSchemaMock, + DATE_NOW, + IMMUTABLE, + VERSION, +} from '../list_schema/index.mock'; + +// TODO: Once these mocks are available, use them instead of hand mocking, https://github.com/elastic/kibana/issues/100715 +// const mockKibanaHttpService = coreMock.createStart().http; +// import { coreMock } from '../../../../../../../src/core/public/mocks'; +const mockKibanaHttpService = jest.fn(); const mockStart = jest.fn(); const mockKeywordList: ListSchema = { @@ -35,7 +41,6 @@ jest.mock('@kbn/securitysolution-list-hooks', () => { return { ...originalModule, - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type useFindLists: () => ({ error: undefined, loading: false, diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx similarity index 80% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx index 34f67f2d402164..4064ff11962bde 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx @@ -1,21 +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 { getGenericComboBoxProps } from '@kbn/securitysolution-autocomplete'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { filterFieldToList } from '../filter_field_to_list'; +import { getGenericComboBoxProps } from '../get_generic_combo_box_props'; -import { filterFieldToList } 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/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx index 50f052c689d666..d695088245622f 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx @@ -6,9 +6,430 @@ * Side Public License, v 1. */ -describe('field_value_match', () => { - test('Tests should be ported', () => { - // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx here once mocks are figured out and kbn package mocks are figured out - expect(true).toBe(true); +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'; + +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({ + autocompleteService: autocompleteStartMock, + fieldValue: '', + indexPattern: { + fields, + id: '1234', + title: 'logstash-*', + }, + operatorType: 'match', + query: 'value 1', + selectedField: getField('machine.os.raw'), + }); + }); + + 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/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx index 0d7df7e351b585..8199967489515f 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx @@ -18,16 +18,16 @@ import { uniq } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -// TODO: I have to use any here for now, but once this is available below, we should use the correct types +// 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 +// 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 * as i18n from '../translations'; import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; import { getGenericComboBoxProps, diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx similarity index 91% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx index 8aa1f18b695a0e..a3ca97874908e1 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -10,18 +11,18 @@ import { ReactWrapper, mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { act } from '@testing-library/react'; -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { AutocompleteFieldMatchAnyComponent } from '.'; +import { getField, fields } from '../fields/index.mock'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { autocompleteStartMock } from '../autocomplete/index.mock'; -import { AutocompleteFieldMatchAnyComponent } from './field_value_match_any'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); - -jest.mock('./hooks/use_field_value_autocomplete'); +jest.mock('../hooks/use_field_value_autocomplete', () => { + const actual = jest.requireActual('../hooks/use_field_value_autocomplete'); + return { + ...actual, + useFieldValueAutocomplete: jest.fn(), + }; +}); describe('AutocompleteFieldMatchAnyComponent', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx similarity index 86% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx index c1820d584bc08a..338c4baa8bc6fc 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx @@ -1,22 +1,32 @@ /* * 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'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { uniq } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { getGenericComboBoxProps, paramIsValid } from '@kbn/securitysolution-autocomplete'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; - -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +type AutocompleteStart = any; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; + +import * as i18n from '../translations'; +import { + getGenericComboBoxProps, + GetGenericComboBoxPropsReturn, +} from '../get_generic_combo_box_props'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { paramIsValid } from '../param_is_valid'; interface AutocompleteFieldMatchAnyProps { placeholder: string; diff --git a/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts new file mode 100644 index 00000000000000..5938ed34547a1f --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts @@ -0,0 +1,313 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Copied from "src/plugins/data/common/index_patterns/fields/fields.mocks.ts" +// but without types. +// TODO: This should move out once those mocks are directly useable or in their own package, https://github.com/elastic/kibana/issues/100715 + +export const fields = [ + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'ssl', + type: 'boolean', + esTypes: ['boolean'], + count: 20, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + count: 30, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'time', + type: 'date', + esTypes: ['date'], + count: 30, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '@tags', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'utc_time', + type: 'date', + esTypes: ['date'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'phpmemory', + type: 'number', + esTypes: ['integer'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'ip', + type: 'ip', + esTypes: ['ip'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'request_body', + type: 'attachment', + esTypes: ['attachment'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'point', + type: 'geo_point', + esTypes: ['geo_point'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'area', + type: 'geo_shape', + esTypes: ['geo_shape'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'hashed', + type: 'murmur3', + esTypes: ['murmur3'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'geo.coordinates', + type: 'geo_point', + esTypes: ['geo_point'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'extension', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'machine.os', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'machine.os.raw', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { multi: { parent: 'machine.os' } }, + }, + { + name: 'geo.src', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '_id', + type: 'string', + esTypes: ['_id'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: '_type', + type: 'string', + esTypes: ['_type'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: '_source', + type: '_source', + esTypes: ['_source'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'non-filterable', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'non-sortable', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'custom_user_field', + type: 'conflict', + esTypes: ['long', 'text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'script string', + type: 'string', + count: 0, + scripted: true, + script: "'i am a string'", + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script number', + type: 'number', + count: 0, + scripted: true, + script: '1234', + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script date', + type: 'date', + count: 0, + scripted: true, + script: '1234', + lang: 'painless', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script murmur3', + type: 'murmur3', + count: 0, + scripted: true, + script: '1234', + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'nestedField.child', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'nestedField' } }, + }, + { + name: 'nestedField.nestedChild.doublyNestedChild', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'nestedField.nestedChild' } }, + }, +]; + +export const getField = (name: string) => fields.find((field) => field.name === name); diff --git a/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts new file mode 100644 index 00000000000000..1022849ffda360 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { filterFieldToList } from '.'; + +import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { getListResponseMock } from '../list_schema/index.mock'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +describe('#filterFieldToList', () => { + test('it returns empty array if given a undefined for field', () => { + const filter = filterFieldToList([], undefined); + expect(filter).toEqual([]); + }); + + test('it returns empty array if filed does not contain esTypes', () => { + const field: IFieldType = { name: 'some-name', type: 'some-type' }; + const filter = filterFieldToList([], field); + expect(filter).toEqual([]); + }); + + test('it returns single filtered list of ip_range -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of ip -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'ip' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of keyword -> keyword', () => { + const field: IFieldType = { esTypes: ['keyword'], name: 'some-name', type: 'keyword' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'keyword' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of text -> text', () => { + const field: IFieldType = { esTypes: ['text'], name: 'some-name', type: 'text' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'text' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns 2 filtered lists of ip_range -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const listItem2: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const filter = filterFieldToList([listItem1, listItem2], field); + const expected: ListSchema[] = [listItem1, listItem2]; + expect(filter).toEqual(expected); + }); + + test('it returns 1 filtered lists of ip_range -> ip if the 2nd is not compatible type', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const listItem2: ListSchema = { ...getListResponseMock(), type: 'text' }; + const filter = filterFieldToList([listItem1, listItem2], field); + const expected: ListSchema[] = [listItem1]; + expect(filter).toEqual(expected); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts new file mode 100644 index 00000000000000..b2e48c25f9b51f --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { typeMatch } from '../type_match'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +/** + * Given an array of lists and optionally a field this will return all + * the lists that match against the field based on the types from the field + * @param lists The lists to match against the field + * @param field The field to check against the list to see if they are compatible + */ +export const filterFieldToList = (lists: ListSchema[], field?: IFieldType): ListSchema[] => { + if (field != null) { + const { esTypes = [] } = field; + return lists.filter(({ type }) => esTypes.some((esType: string) => typeMatch(type, esType))); + } else { + return []; + } +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts new file mode 100644 index 00000000000000..e473df104fa6a4 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + doesNotExistOperator, + EXCEPTION_OPERATORS, + existsOperator, + isNotOperator, + isOperator, +} from '@kbn/securitysolution-list-utils'; +import { getOperators } from '.'; +import { getField } from '../fields/index.mock'; + +describe('#getOperators', () => { + test('it returns "isOperator" if passed in field is "undefined"', () => { + const operator = getOperators(undefined); + + expect(operator).toEqual([isOperator]); + }); + + test('it returns expected operators when field type is "boolean"', () => { + const operator = getOperators(getField('ssl')); + + expect(operator).toEqual([isOperator, isNotOperator, existsOperator, doesNotExistOperator]); + }); + + test('it returns "isOperator" when field type is "nested"', () => { + const operator = getOperators({ + aggregatable: false, + count: 0, + esTypes: ['text'], + name: 'nestedField', + readFromDocValues: false, + scripted: false, + searchable: true, + subType: { nested: { path: 'nestedField' } }, + type: 'nested', + }); + + expect(operator).toEqual([isOperator]); + }); + + test('it returns all operator types when field type is not null, boolean, or nested', () => { + const operator = getOperators(getField('machine.os.raw')); + + expect(operator).toEqual(EXCEPTION_OPERATORS); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts new file mode 100644 index 00000000000000..39d2779e2dc44f --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +import { + EXCEPTION_OPERATORS, + OperatorOption, + doesNotExistOperator, + existsOperator, + isNotOperator, + isOperator, +} from '@kbn/securitysolution-list-utils'; + +/** + * Returns the appropriate operators given a field type + * + * @param field IFieldType selected field + * + */ +export const getOperators = (field: IFieldType | undefined): OperatorOption[] => { + if (field == null) { + return [isOperator]; + } else if (field.type === 'boolean') { + return [isOperator, isNotOperator, existsOperator, doesNotExistOperator]; + } else if (field.type === 'nested') { + return [isOperator]; + } else { + return EXCEPTION_OPERATORS; + } +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts index 84fd3c51649161..534daa021cf4a8 100644 --- a/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts @@ -6,9 +6,341 @@ * 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 { + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn, + 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', () => { - test('Tests should be ported', () => { - // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts here once mocks are figured out and kbn package mocks are figured out - expect(true).toBe(true); + const onErrorMock = jest.fn(); + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + afterEach(() => { + onErrorMock.mockClear(); + getValueSuggestionsMock.mockClear(); + }); + + test('initializes hook', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: getValueSuggestionsMock, + }, + fieldValue: '', + indexPattern: undefined, + operatorType: OperatorTypeEnum.MATCH, + query: '', + selectedField: undefined, + }) + ); + 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({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: getValueSuggestionsMock, + }, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + operatorType: OperatorTypeEnum.EXISTS, + query: '', + selectedField: getField('machine.os'), + }) + ); + 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({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: getValueSuggestionsMock, + }, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + operatorType: OperatorTypeEnum.EXISTS, + query: '', + selectedField: undefined, + }) + ); + 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({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: getValueSuggestionsMock, + }, + fieldValue: '', + indexPattern: undefined, + operatorType: OperatorTypeEnum.EXISTS, + query: '', + selectedField: getField('machine.os'), + }) + ); + 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([]); + + await act(async () => { + const { signal } = new AbortController(); + const { waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: suggestionsMock, + }, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + operatorType: OperatorTypeEnum.MATCH, + query: '', + selectedField: { ...getField('nestedField.child'), name: 'child' }, + }) + ); + // 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({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: getValueSuggestionsMock, + }, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + operatorType: OperatorTypeEnum.MATCH, + query: '', + selectedField: getField('ssl'), + }) + ); + // 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([]); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: suggestionsMock, + }, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + operatorType: OperatorTypeEnum.MATCH, + query: '', + selectedField: getField('bytes'), + }) + ); + // 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(); + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: getValueSuggestionsMock, + }, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + operatorType: OperatorTypeEnum.MATCH, + query: '', + selectedField: getField('@tags'), + }) + ); + // 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({ + autocompleteService: { + ...autocompleteStartMock, + getValueSuggestions: getValueSuggestionsMock, + }, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + operatorType: OperatorTypeEnum.MATCH, + query: '', + selectedField: getField('@tags'), + }) + ); + // 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'), + patterns: stubIndexPatternWithFields, + searchQuery: '', + value: 'hello', + }); + } + + 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/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts index 9a850e8bc20564..b4dec1615e3ed4 100644 --- a/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts @@ -10,11 +10,11 @@ import { useEffect, useRef, useState } from 'react'; import { debounce } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -// TODO: I have to use any here for now, but once this is available below, we should use the correct types +// 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 +// 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; diff --git a/packages/kbn-securitysolution-autocomplete/src/index.ts b/packages/kbn-securitysolution-autocomplete/src/index.ts index 034a29f3d14b67..5fcb3f954189ad 100644 --- a/packages/kbn-securitysolution-autocomplete/src/index.ts +++ b/packages/kbn-securitysolution-autocomplete/src/index.ts @@ -7,7 +7,13 @@ */ export * from './check_empty_value'; export * from './field'; +export * from './field_value_exists'; +export * from './field_value_lists'; export * from './field_value_match'; +export * from './field_value_match_any'; +export * from './filter_field_to_list'; export * from './get_generic_combo_box_props'; +export * from './get_operators'; export * from './hooks'; +export * from './operator'; export * from './param_is_valid'; diff --git a/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts new file mode 100644 index 00000000000000..fb629ad2f946ed --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FoundListSchema, ListSchema } from '@kbn/securitysolution-io-ts-list-types'; + +// TODO: Once this mock is available within packages, use it instead, https://github.com/elastic/kibana/issues/100715 +// import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; +export const getFoundListSchemaMock = (): FoundListSchema => ({ + cursor: '123', + data: [getListResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); + +// TODO: Once these mocks are available from packages use it instead, https://github.com/elastic/kibana/issues/100715 +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; +export const USER = 'some user'; +export const IMMUTABLE = false; +export const VERSION = 1; +export const DESCRIPTION = 'some description'; +export const TIE_BREAKER = '6a76b69d-80df-4ab2-8c3e-85f466b06a0e'; +export const LIST_ID = 'some-list-id'; +export const META = {}; +export const TYPE = 'ip'; +export const NAME = 'some name'; + +// TODO: Once this mock is available within packages, use it instead, https://github.com/elastic/kibana/issues/100715 +// import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; +export const getListResponseMock = (): ListSchema => ({ + _version: undefined, + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + deserializer: undefined, + id: LIST_ID, + immutable: IMMUTABLE, + meta: META, + name: NAME, + serializer: undefined, + tie_breaker_id: TIE_BREAKER, + type: TYPE, + updated_at: DATE_NOW, + updated_by: USER, + version: VERSION, +}); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx b/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx similarity index 95% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx index dadde8800b67f6..fed7007b496361 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -10,11 +11,10 @@ import { mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { isNotOperator, isOperator } from '@kbn/securitysolution-list-utils'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; +import { OperatorComponent } from '.'; +import { getField } from '../fields/index.mock'; -import { OperatorComponent } from './operator'; - -describe('OperatorComponent', () => { +describe('operator', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( { - test('Tests should be ported', () => { - // TODO: Port all the tests from: x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts here once mocks are figured out and kbn package mocks are figured out - expect(true).toBe(true); + 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 index bd777356866a62..5b596b4b624086 100644 --- a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts +++ b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts @@ -9,11 +9,11 @@ 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 +// 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'; +import * as i18n from '../translations'; /** * Very basic validation for values diff --git a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts deleted file mode 100644 index 74bbbd6d16872e..00000000000000 --- a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/translations.ts +++ /dev/null @@ -1,17 +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 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 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/field_value_match/translations.ts b/packages/kbn-securitysolution-autocomplete/src/translations/index.ts similarity index 63% rename from packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts rename to packages/kbn-securitysolution-autocomplete/src/translations/index.ts index 7433a4489ca828..35d6531be51bdc 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_match/translations.ts +++ b/packages/kbn-securitysolution-autocomplete/src/translations/index.ts @@ -15,3 +15,15 @@ export const LOADING = i18n.translate('autocomplete.loadingDescription', { export const SELECT_FIELD_FIRST = i18n.translate('autocomplete.selectField', { defaultMessage: 'Please select a field first...', }); + +export const FIELD_REQUIRED_ERR = i18n.translate('autocomplete.fieldRequiredError', { + defaultMessage: 'Value cannot be empty', +}); + +export const NUMBER_ERR = i18n.translate('autocomplete.invalidNumberError', { + defaultMessage: 'Not a valid number', +}); + +export const DATE_ERR = i18n.translate('autocomplete.invalidDateError', { + defaultMessage: 'Not a valid date', +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts new file mode 100644 index 00000000000000..4694313720c792 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { typeMatch } from '.'; + +describe('type_match', () => { + test('ip -> ip is true', () => { + expect(typeMatch('ip', 'ip')).toEqual(true); + }); + + test('keyword -> keyword is true', () => { + expect(typeMatch('keyword', 'keyword')).toEqual(true); + }); + + test('text -> text is true', () => { + expect(typeMatch('text', 'text')).toEqual(true); + }); + + test('ip_range -> ip is true', () => { + expect(typeMatch('ip_range', 'ip')).toEqual(true); + }); + + test('date_range -> date is true', () => { + expect(typeMatch('date_range', 'date')).toEqual(true); + }); + + test('double_range -> double is true', () => { + expect(typeMatch('double_range', 'double')).toEqual(true); + }); + + test('float_range -> float is true', () => { + expect(typeMatch('float_range', 'float')).toEqual(true); + }); + + test('integer_range -> integer is true', () => { + expect(typeMatch('integer_range', 'integer')).toEqual(true); + }); + + test('long_range -> long is true', () => { + expect(typeMatch('long_range', 'long')).toEqual(true); + }); + + test('ip -> date is false', () => { + expect(typeMatch('ip', 'date')).toEqual(false); + }); + + test('long -> float is false', () => { + expect(typeMatch('long', 'float')).toEqual(false); + }); + + test('integer -> long is false', () => { + expect(typeMatch('integer', 'long')).toEqual(false); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts b/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts new file mode 100644 index 00000000000000..d5476f3b32b49b --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; + +/** + * Given an input list type and a string based ES type this will match + * if they're exact or if they are compatible with a range + * @param type The type to match against the esType + * @param esType The ES type to match with + */ +export const typeMatch = (type: Type, esType: string): boolean => { + return ( + type === esType || + (type === 'ip_range' && esType === 'ip') || + (type === 'date_range' && esType === 'date') || + (type === 'double_range' && esType === 'double') || + (type === 'float_range' && esType === 'float') || + (type === 'integer_range' && esType === 'integer') || + (type === 'long_range' && esType === 'long') + ); +}; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/README.md b/x-pack/plugins/lists/public/exceptions/components/autocomplete/README.md deleted file mode 100644 index fb500ca0761e33..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/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 - -``` \ No newline at end of file diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx deleted file mode 100644 index 3c316193f583df..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/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 { FieldComponent } from '@kbn/securitysolution-autocomplete'; - -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -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/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx deleted file mode 100644 index ec34de26791ebf..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx +++ /dev/null @@ -1,443 +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 { ReactWrapper, mount } from 'enzyme'; -import { EuiComboBox, EuiComboBoxOptionOption, EuiSuperSelect } from '@elastic/eui'; -import { act } from '@testing-library/react'; -import { - AutocompleteFieldMatchComponent, - useFieldValueAutocomplete, -} from '@kbn/securitysolution-autocomplete'; - -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -// TODO: Mock this -// jest.mock('../hooks/use_field_value_autocomplete'); - -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); - -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.skip('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.skip('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({ - autocompleteService: autocompleteStartMock, - fieldValue: '', - indexPattern: { - fields, - id: '1234', - title: 'logstash-*', - }, - operatorType: 'match', - query: 'value 1', - selectedField: getField('machine.os.raw'), - }); - }); - - 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/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 585d0fc533a428..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts +++ /dev/null @@ -1,386 +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 { - checkEmptyValue, - getGenericComboBoxProps, - paramIsValid, -} from '@kbn/securitysolution-autocomplete'; - -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 { filterFieldToList, getOperators, 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 fb8e2183ac0b51..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts +++ /dev/null @@ -1,69 +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 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'; - -/** - * 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; - } -}; - -/** - * 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/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts deleted file mode 100644 index 0335ffa55d2a28..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ /dev/null @@ -1,334 +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 { 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', () => { - const onErrorMock = jest.fn(); - const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); - - afterEach(() => { - onErrorMock.mockClear(); - getValueSuggestionsMock.mockClear(); - }); - - test('initializes hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: getValueSuggestionsMock, - }, - fieldValue: '', - indexPattern: undefined, - operatorType: OperatorTypeEnum.MATCH, - query: '', - selectedField: undefined, - }) - ); - 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({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: getValueSuggestionsMock, - }, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - operatorType: OperatorTypeEnum.EXISTS, - query: '', - selectedField: getField('machine.os'), - }) - ); - 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({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: getValueSuggestionsMock, - }, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - operatorType: OperatorTypeEnum.EXISTS, - query: '', - selectedField: undefined, - }) - ); - 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({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: getValueSuggestionsMock, - }, - fieldValue: '', - indexPattern: undefined, - operatorType: OperatorTypeEnum.EXISTS, - query: '', - selectedField: getField('machine.os'), - }) - ); - 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([]); - - await act(async () => { - const { signal } = new AbortController(); - const { waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: suggestionsMock, - }, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - operatorType: OperatorTypeEnum.MATCH, - query: '', - selectedField: { ...getField('nestedField.child'), name: 'child' }, - }) - ); - // 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({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: getValueSuggestionsMock, - }, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - operatorType: OperatorTypeEnum.MATCH, - query: '', - selectedField: getField('ssl'), - }) - ); - // 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([]); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: suggestionsMock, - }, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - operatorType: OperatorTypeEnum.MATCH, - query: '', - selectedField: getField('bytes'), - }) - ); - // 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(); - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: getValueSuggestionsMock, - }, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - operatorType: OperatorTypeEnum.MATCH, - query: '', - selectedField: getField('@tags'), - }) - ); - // 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({ - autocompleteService: { - ...autocompleteStartMock, - getValueSuggestions: getValueSuggestionsMock, - }, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - operatorType: OperatorTypeEnum.MATCH, - query: '', - selectedField: getField('@tags'), - }) - ); - // 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'), - patterns: stubIndexPatternWithFields, - searchQuery: '', - value: 'hello', - }); - } - - 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/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts deleted file mode 100644 index 63d3925d6d64d3..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/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, 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'; - -interface FuncArgs { - fieldSelected: IFieldType | undefined; - patterns: IIndexPattern | undefined; - searchQuery: string; - value: string | string[] | undefined; -} - -type Func = (args: FuncArgs) => void; - -export type UseFieldValueAutocompleteReturn = [boolean, boolean, string[], Func | null]; - -export interface UseFieldValueAutocompleteProps { - autocompleteService: AutocompleteStart; - fieldValue: string | string[] | undefined; - indexPattern: IIndexPattern | undefined; - operatorType: OperatorTypeEnum; - query: string; - selectedField: IFieldType | undefined; -} -/** - * 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, - operatorType, - fieldValue, - query, - indexPattern, - autocompleteService, -}: UseFieldValueAutocompleteProps): UseFieldValueAutocompleteReturn => { - 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, patterns, searchQuery }: 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 autocompleteService.getValueSuggestions({ - field, - indexPattern: patterns, - 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, - patterns: indexPattern, - searchQuery: query, - value: fieldValue, - }); - } - - updateSuggestions.current = fetchSuggestions; - - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [selectedField, operatorType, fieldValue, indexPattern, query, autocompleteService]); - - return [isLoading, isSuggestingValues, suggestions, updateSuggestions.current]; -}; 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 21478d14819047..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx +++ /dev/null @@ -1,11 +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 { OperatorComponent } from './operator'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts deleted file mode 100644 index 065239246d3299..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const LOADING = i18n.translate('xpack.lists.autocomplete.loadingDescription', { - defaultMessage: 'Loading...', -}); - -export const SELECT_FIELD_FIRST = i18n.translate('xpack.lists.autocomplete.selectField', { - defaultMessage: 'Please select a field first...', -}); - -export const FIELD_REQUIRED_ERR = i18n.translate('xpack.lists.autocomplete.fieldRequiredError', { - defaultMessage: 'Value cannot be empty', -}); - -export const NUMBER_ERR = i18n.translate('xpack.lists.autocomplete.invalidNumberError', { - defaultMessage: 'Not a valid number', -}); - -export const DATE_ERR = i18n.translate('xpack.lists.autocomplete.invalidDateError', { - defaultMessage: 'Not a valid date', -}); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts deleted file mode 100644 index 07f1903fb70e1c..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiComboBoxOptionOption } from '@elastic/eui'; - -export interface GetGenericComboBoxPropsReturn { - comboOptions: EuiComboBoxOptionOption[]; - labels: string[]; - selectedComboOptions: EuiComboBoxOptionOption[]; -} diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 7abdf4f88cf3d7..d7741b3fe0ff10 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -28,17 +28,17 @@ import { 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 { OperatorComponent } from '../autocomplete/operator'; -import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_exists'; -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'; From e9a32f4db9dbd001b01e09c18f2dda9909a68bc6 Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Tue, 20 Jul 2021 14:06:56 -0600 Subject: [PATCH 7/8] Fixed i81n keys --- .../translations/translations/ja-JP.json | 22 +++++-------------- .../translations/translations/zh-CN.json | 22 +++++-------------- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0cf63b94f17583..f2ed422cecf6ad 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6081,8 +6081,6 @@ "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "ワークパッドを作成できませんでした", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "ID でワークパッドを読み込めませんでした", - "expressionError.errorComponent.description": "表現が失敗し次のメッセージが返されました:", - "expressionError.errorComponent.title": "おっと!表現が失敗しました", "xpack.canvas.expression.cancelButtonLabel": "キャンセル", "xpack.canvas.expression.closeButtonLabel": "閉じる", "xpack.canvas.expression.learnLinkText": "表現構文の詳細", @@ -6494,15 +6492,11 @@ "xpack.canvas.renderer.advancedFilter.displayName": "高度なフィルター", "xpack.canvas.renderer.advancedFilter.helpDescription": "Canvas フィルター表現をレンダリングします。", "xpack.canvas.renderer.advancedFilter.inputPlaceholder": "フィルター表現を入力", - "expressionError.renderer.debug.displayName": "デバッグ", - "expressionError.renderer.debug.helpDescription": "デバッグアウトプットをフォーマットされた {JSON} としてレンダリングします", "xpack.canvas.renderer.dropdownFilter.displayName": "ドロップダウンフィルター", "xpack.canvas.renderer.dropdownFilter.helpDescription": "「{exactly}」フィルターの値を選択できるドロップダウンです", "xpack.canvas.renderer.dropdownFilter.matchAllOptionLabel": "すべて", "xpack.canvas.renderer.embeddable.displayName": "埋め込み可能", "xpack.canvas.renderer.embeddable.helpDescription": "Kibana の他の部分から埋め込み可能な保存済みオブジェクトをレンダリングします", - "expressionError.renderer.error.displayName": "エラー情報", - "expressionError.renderer.error.helpDescription": "エラーデータをユーザーにわかるようにレンダリングします", "xpack.canvas.renderer.image.displayName": "画像", "xpack.canvas.renderer.image.helpDescription": "画像をレンダリングします", "xpack.canvas.renderer.markdown.displayName": "マークダウン", @@ -6986,6 +6980,12 @@ "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "説明", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "テンプレート名", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "タグ", + "expressionError.errorComponent.description": "表現が失敗し次のメッセージが返されました:", + "expressionError.errorComponent.title": "おっと!表現が失敗しました", + "expressionError.renderer.debug.displayName": "デバッグ", + "expressionError.renderer.debug.helpDescription": "デバッグアウトプットをフォーマットされた {JSON} としてレンダリングします", + "expressionError.renderer.error.displayName": "エラー情報", + "expressionError.renderer.error.helpDescription": "エラーデータをユーザーにわかるようにレンダリングします", "expressionRevealImage.functions.revealImage.args.emptyImageHelpText": "表示される背景画像です。画像アセットは「{BASE64}」データ {URL} として提供するか、部分式で渡します。", "expressionRevealImage.functions.revealImage.args.imageHelpText": "表示する画像です。画像アセットは{BASE64}データ{URL}として提供するか、部分式で渡します。", "expressionRevealImage.functions.revealImage.args.originHelpText": "画像で埋め始める位置です。たとえば、{list}、または {end}です。", @@ -12918,11 +12918,6 @@ "xpack.licensing.welcomeBanner.licenseIsExpiredTitle": "ご使用の{licenseType}ライセンスは期限切れです", "xpack.lists.andOrBadge.andLabel": "AND", "xpack.lists.andOrBadge.orLabel": "OR", - "xpack.lists.autocomplete.fieldRequiredError": "値を空にすることはできません", - "xpack.lists.autocomplete.invalidDateError": "有効な日付ではありません", - "xpack.lists.autocomplete.invalidNumberError": "有効な数値ではありません", - "xpack.lists.autocomplete.loadingDescription": "読み込み中...", - "xpack.lists.autocomplete.selectField": "最初にフィールドを選択してください...", "xpack.lists.exceptions.andDescription": "AND", "xpack.lists.exceptions.builder.addNestedDescription": "ネストされた条件を追加", "xpack.lists.exceptions.builder.addNonNestedDescription": "ネストされていない条件を追加", @@ -18714,11 +18709,6 @@ "xpack.securitySolution.authenticationsTable.user": "ユーザー", "xpack.securitySolution.authz.mlUnavailable": "機械学習プラグインが使用できません。プラグインを有効にしてください。", "xpack.securitySolution.authz.userIsNotMlAdminMessage": "現在のユーザーは機械学習管理者ではありません。", - "xpack.securitySolution.autocomplete.fieldRequiredError": "値を空にすることはできません", - "xpack.securitySolution.autocomplete.invalidDateError": "有効な日付ではありません", - "xpack.securitySolution.autocomplete.invalidNumberError": "有効な数値ではありません", - "xpack.securitySolution.autocomplete.loadingDescription": "読み込み中...", - "xpack.securitySolution.autocomplete.selectField": "最初にフィールドを選択してください...", "xpack.securitySolution.beatFields.errorSearchDescription": "Beatフィールドの取得でエラーが発生しました", "xpack.securitySolution.beatFields.failSearchDescription": "Beat フィールドで検索を実行できませんでした", "xpack.securitySolution.callouts.dismissButton": "閉じる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6659c51867fa67..2762b1f5ea12f5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6120,8 +6120,6 @@ "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "无法创建 Workpad", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "无法加载具有以下 ID 的 Workpad", - "expressionError.errorComponent.description": "表达式失败,并显示消息:", - "expressionError.errorComponent.title": "哎哟!表达式失败", "xpack.canvas.expression.cancelButtonLabel": "取消", "xpack.canvas.expression.closeButtonLabel": "关闭", "xpack.canvas.expression.learnLinkText": "学习表达式语法", @@ -6534,15 +6532,11 @@ "xpack.canvas.renderer.advancedFilter.displayName": "高级筛选", "xpack.canvas.renderer.advancedFilter.helpDescription": "呈现 Canvas 筛选表达式", "xpack.canvas.renderer.advancedFilter.inputPlaceholder": "输入筛选表达式", - "expressionError.renderer.debug.displayName": "故障排查", - "expressionError.renderer.debug.helpDescription": "将故障排查输出呈现为带格式的 {JSON}", "xpack.canvas.renderer.dropdownFilter.displayName": "下拉列表筛选", "xpack.canvas.renderer.dropdownFilter.helpDescription": "可以从其中为“{exactly}”筛选选择值的下拉列表", "xpack.canvas.renderer.dropdownFilter.matchAllOptionLabel": "任意", "xpack.canvas.renderer.embeddable.displayName": "可嵌入", "xpack.canvas.renderer.embeddable.helpDescription": "从 Kibana 的其他部分呈现可嵌入的已保存对象", - "expressionError.renderer.error.displayName": "错误信息", - "expressionError.renderer.error.helpDescription": "以用户友好的方式呈现错误数据", "xpack.canvas.renderer.image.displayName": "图像", "xpack.canvas.renderer.image.helpDescription": "呈现图像", "xpack.canvas.renderer.markdown.displayName": "Markdown", @@ -7034,6 +7028,12 @@ "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "描述", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "模板名称", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "标签", + "expressionError.errorComponent.description": "表达式失败,并显示消息:", + "expressionError.errorComponent.title": "哎哟!表达式失败", + "expressionError.renderer.debug.displayName": "故障排查", + "expressionError.renderer.debug.helpDescription": "将故障排查输出呈现为带格式的 {JSON}", + "expressionError.renderer.error.displayName": "错误信息", + "expressionError.renderer.error.helpDescription": "以用户友好的方式呈现错误数据", "expressionRevealImage.functions.revealImage.args.emptyImageHelpText": "要显示的可选背景图像。以 `{BASE64}` 数据 {URL} 的形式提供图像资产或传入子表达式。", "expressionRevealImage.functions.revealImage.args.imageHelpText": "要显示的图像。以 {BASE64} 数据 {URL} 的形式提供图像资产或传入子表达式。", "expressionRevealImage.functions.revealImage.args.originHelpText": "要开始图像填充的位置。例如 {list} 或 {end}。", @@ -13092,11 +13092,6 @@ "xpack.licensing.welcomeBanner.licenseIsExpiredTitle": "您的{licenseType}许可已过期", "xpack.lists.andOrBadge.andLabel": "且", "xpack.lists.andOrBadge.orLabel": "OR", - "xpack.lists.autocomplete.fieldRequiredError": "值不能为空", - "xpack.lists.autocomplete.invalidDateError": "不是有效日期", - "xpack.lists.autocomplete.invalidNumberError": "不是有效数字", - "xpack.lists.autocomplete.loadingDescription": "正在加载……", - "xpack.lists.autocomplete.selectField": "请首先选择字段......", "xpack.lists.exceptions.andDescription": "且", "xpack.lists.exceptions.builder.addNestedDescription": "添加嵌套条件", "xpack.lists.exceptions.builder.addNonNestedDescription": "添加非嵌套条件", @@ -18982,11 +18977,6 @@ "xpack.securitySolution.authenticationsTable.user": "用户", "xpack.securitySolution.authz.mlUnavailable": "Machine Learning 插件不可用。请尝试启用插件。", "xpack.securitySolution.authz.userIsNotMlAdminMessage": "当前用户不是 Machine Learning 管理员。", - "xpack.securitySolution.autocomplete.fieldRequiredError": "值不能为空", - "xpack.securitySolution.autocomplete.invalidDateError": "不是有效日期", - "xpack.securitySolution.autocomplete.invalidNumberError": "不是有效数字", - "xpack.securitySolution.autocomplete.loadingDescription": "正在加载……", - "xpack.securitySolution.autocomplete.selectField": "请首先选择字段......", "xpack.securitySolution.beatFields.errorSearchDescription": "获取 Beat 字段时发生错误", "xpack.securitySolution.beatFields.failSearchDescription": "无法对 Beat 字段执行搜索", "xpack.securitySolution.callouts.dismissButton": "关闭", From cbc133fa0f6d80c7135aa91e02983de8949f30b9 Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Thu, 22 Jul 2021 09:40:20 -0600 Subject: [PATCH 8/8] Fixing those i18n keys (looks like I am going to conflict for a while) --- .../translations/translations/ja-JP.json | 23 ++++--------------- .../translations/translations/zh-CN.json | 15 +----------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6d2a13c2b602a2..2d37576bae1fd8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6545,10 +6545,7 @@ "xpack.canvas.error.workpadDropzone.tooManyFilesErrorMessage": "同時にアップロードできるファイルは1つだけです。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "ワークパッドを作成できませんでした", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "ID でワークパッドを読み込めませんでした", -<<<<<<< HEAD -======= "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした", ->>>>>>> master "xpack.canvas.expression.cancelButtonLabel": "キャンセル", "xpack.canvas.expression.closeButtonLabel": "閉じる", "xpack.canvas.expression.learnLinkText": "表現構文の詳細", @@ -7463,20 +7460,9 @@ "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "説明", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "テンプレート名", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "タグ", - "expressionError.errorComponent.description": "表現が失敗し次のメッセージが返されました:", - "expressionError.errorComponent.title": "おっと!表現が失敗しました", - "expressionError.renderer.debug.helpDescription": "デバッグアウトプットをフォーマットされた {JSON} としてレンダリングします", - "expressionError.renderer.error.displayName": "エラー情報", - "expressionError.renderer.error.helpDescription": "エラーデータをユーザーにわかるようにレンダリングします", - "expressionRevealImage.functions.revealImage.args.emptyImageHelpText": "表示される背景画像です。画像アセットは「{BASE64}」データ {URL} として提供するか、部分式で渡します。", - "expressionRevealImage.functions.revealImage.args.imageHelpText": "表示する画像です。画像アセットは{BASE64}データ{URL}として提供するか、部分式で渡します。", - "expressionRevealImage.functions.revealImage.args.originHelpText": "画像で埋め始める位置です。たとえば、{list}、または {end}です。", - "expressionRevealImage.functions.revealImage.invalidPercentErrorMessage": "無効な値:「{percent}」。パーセンテージは 0 と 1 の間でなければなりません ", - "expressionRevealImage.functions.revealImageHelpText": "画像表示エレメントを構成します。", - "expressionRevealImage.renderer.revealImage.displayName": "画像の部分表示", - "expressionRevealImage.renderer.revealImage.helpDescription": "カスタムゲージスタイルチャートを作成するため、画像のパーセンテージを表示します", "expressionShape.functions.shape.args.borderHelpText": "図形の外郭の {SVG} カラーです。", "expressionShape.functions.shape.args.borderWidthHelpText": "境界の太さです。", + "expressionShape.functions.shape.args.fillHelpText": "図形を塗りつぶす {SVG} カラーです。", "expressionShape.functions.shape.args.maintainAspectHelpText": "図形の元の横縦比を維持しますか?", "expressionShape.functions.shape.args.shapeHelpText": "図形を選択します。", "expressionShape.functions.shapeHelpText": "図形を作成します。", @@ -7654,6 +7640,7 @@ "xpack.cases.connecors.register.duplicateCaseConnectorErrorMessage": "オブジェクトタイプ「{id}」はすでに登録されています。", "xpack.cases.connectors.cases.externalIncidentAdded": " ({date}に{user}が追加) ", "xpack.cases.connectors.cases.externalIncidentCreated": " ({date}に{user}が作成) ", + "xpack.cases.connectors.cases.externalIncidentDefault": " ({date}に{user}が作成) ", "xpack.cases.connectors.cases.externalIncidentUpdated": " ({date}に{user}が更新) ", "xpack.cases.connectors.cases.title": "ケース", "xpack.cases.connectors.jira.issueTypesSelectFieldLabel": "問題タイプ", @@ -16872,7 +16859,7 @@ "xpack.ml.ruleEditor.scopeSection.noPermissionToViewFilterListsTitle": "フィルターリストを表示するパーミッションがありません", "xpack.ml.ruleEditor.scopeSection.scopeTitle": "範囲", "xpack.ml.ruleEditor.selectRuleAction.createRuleLinkText": "ルールを作成", - "xpack.ml.ruleEditor.selectRuleAction.orText": "OR ", + "xpack.ml.ruleEditor.selectRuleAction.orText": "OR ", "xpack.ml.ruleEditor.typicalAppliesTypeText": "通常", "xpack.ml.sampleDataLinkLabel": "ML ジョブ", "xpack.ml.settings.anomalyDetection.anomalyDetectionTitle": "異常検知", @@ -22508,9 +22495,9 @@ "xpack.securitySolution.open.timeline.showingLabel": "表示中:", "xpack.securitySolution.open.timeline.singleTemplateLabel": "テンプレート", "xpack.securitySolution.open.timeline.singleTimelineLabel": "タイムライン", - "xpack.securitySolution.open.timeline.successfullyDeletedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} other {{totalTimelines} 個のタイムライン}}の削除が正常に完了しました", + "xpack.securitySolution.open.timeline.successfullyDeletedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} other {{totalTimelines} 個のタイムライン}}の削除が正常に完了しました", "xpack.securitySolution.open.timeline.successfullyDeletedTimelineTemplatesTitle": "{totalTimelineTemplates, plural, =0 {すべてのタイムライン} other {{totalTimelineTemplates}個のタイムラインテンプレート}}が正常に削除されました", - "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} other {{totalTimelines} 個のタイムライン}}のエクスポートが正常に完了しました", + "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} other {{totalTimelines} 個のタイムライン}}のエクスポートが正常に完了しました", "xpack.securitySolution.open.timeline.successfullyExportedTimelineTemplatesTitle": "{totalTimelineTemplates, plural, =0 {すべてのタイムライン} other {{totalTimelineTemplates} タイムラインテンプレート}}が正常にエクスポートされました", "xpack.securitySolution.open.timeline.timelineNameTableHeader": "タイムライン名", "xpack.securitySolution.open.timeline.timelineTemplateNameTableHeader": "テンプレート名", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c6cf7a34c40801..e7975c969e0da6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7509,19 +7509,6 @@ "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "描述", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "模板名称", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "标签", - "expressionError.errorComponent.description": "表达式失败,并显示消息:", - "expressionError.errorComponent.title": "哎哟!表达式失败", - "expressionError.renderer.debug.displayName": "故障排查", - "expressionError.renderer.debug.helpDescription": "将故障排查输出呈现为带格式的 {JSON}", - "expressionError.renderer.error.displayName": "错误信息", - "expressionError.renderer.error.helpDescription": "以用户友好的方式呈现错误数据", - "expressionRevealImage.functions.revealImage.args.emptyImageHelpText": "要显示的可选背景图像。以 `{BASE64}` 数据 {URL} 的形式提供图像资产或传入子表达式。", - "expressionRevealImage.functions.revealImage.args.imageHelpText": "要显示的图像。以 {BASE64} 数据 {URL} 的形式提供图像资产或传入子表达式。", - "expressionRevealImage.functions.revealImage.args.originHelpText": "要开始图像填充的位置。例如 {list} 或 {end}。", - "expressionRevealImage.functions.revealImage.invalidPercentErrorMessage": "无效值:“{percent}”。百分比必须介于 0 和 1 之间", - "expressionRevealImage.functions.revealImageHelpText": "配置图像显示元素。", - "expressionRevealImage.renderer.revealImage.displayName": "图像显示", - "expressionRevealImage.renderer.revealImage.helpDescription": "显示一定百分比的图像,以制作定制的仪表样式图表", "expressionShape.functions.shape.args.borderHelpText": "形状轮廓边框的 {SVG} 颜色。", "expressionShape.functions.shape.args.borderWidthHelpText": "边框的粗细。", "expressionShape.functions.shape.args.fillHelpText": "填充形状的 {SVG} 颜色。", @@ -17107,7 +17094,7 @@ "xpack.ml.ruleEditor.scopeSection.noPermissionToViewFilterListsTitle": "您无权查看筛选列表", "xpack.ml.ruleEditor.scopeSection.scopeTitle": "范围", "xpack.ml.ruleEditor.selectRuleAction.createRuleLinkText": "创建规则", - "xpack.ml.ruleEditor.selectRuleAction.orText": "或 ", + "xpack.ml.ruleEditor.selectRuleAction.orText": "或 ", "xpack.ml.ruleEditor.typicalAppliesTypeText": "典型", "xpack.ml.sampleDataLinkLabel": "ML 作业", "xpack.ml.settings.anomalyDetection.anomalyDetectionTitle": "异常检测",