[Security Solutions][Detection Engine] Creates an autocomplete package and moves duplicate code between lists and security_solution there (#105382) (#106612)
## Summary Creates an autocomplete package from `lists` and removes duplicate code between `lists` and `security_solutions` * Consolidates different PR's where we were changing different parts of autocomplete in different ways. * Existing Cypress tests should cover any mistakes hopefully Manual Testing: * Ensure this bug does not crop up again https://github.com/elastic/kibana/pull/87004 * Make sure that the exception list autocomplete looks alright ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios # Conflicts: # x-pack/plugins/translations/translations/ja-JP.json
This commit is contained in:
parent
50b9784dd7
commit
94fc5d1139
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"paths": {
|
||||
"autocomplete": "packages/kbn-securitysolution-autocomplete/src",
|
||||
"console": "src/plugins/console",
|
||||
"core": "src/core",
|
||||
"discover": "src/plugins/discover",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -137,6 +137,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",
|
||||
|
|
|
@ -36,6 +36,7 @@ filegroup(
|
|||
"//packages/kbn-plugin-generator:build",
|
||||
"//packages/kbn-plugin-helpers:build",
|
||||
"//packages/kbn-rule-data-utils:build",
|
||||
"//packages/kbn-securitysolution-autocomplete:build",
|
||||
"//packages/kbn-securitysolution-list-constants:build",
|
||||
"//packages/kbn-securitysolution-io-ts-types:build",
|
||||
"//packages/kbn-securitysolution-io-ts-alerting-types:build",
|
||||
|
|
125
packages/kbn-securitysolution-autocomplete/BUILD.bazel
Normal file
125
packages/kbn-securitysolution-autocomplete/BUILD.bazel
Normal file
|
@ -0,0 +1,125 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
|
||||
|
||||
PKG_BASE_NAME = "kbn-securitysolution-autocomplete"
|
||||
|
||||
PKG_REQUIRE_NAME = "@kbn/securitysolution-autocomplete"
|
||||
|
||||
SOURCE_FILES = glob(
|
||||
[
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
exclude = [
|
||||
"**/*.test.*",
|
||||
"**/*.mock.*",
|
||||
"**/*.mocks.*",
|
||||
],
|
||||
)
|
||||
|
||||
SRCS = SOURCE_FILES
|
||||
|
||||
filegroup(
|
||||
name = "srcs",
|
||||
srcs = SRCS,
|
||||
)
|
||||
|
||||
NPM_MODULE_EXTRA_FILES = [
|
||||
"react/package.json",
|
||||
"package.json",
|
||||
"README.md",
|
||||
]
|
||||
|
||||
SRC_DEPS = [
|
||||
"//packages/kbn-babel-preset",
|
||||
"//packages/kbn-dev-utils",
|
||||
"//packages/kbn-i18n",
|
||||
"//packages/kbn-securitysolution-io-ts-list-types",
|
||||
"//packages/kbn-securitysolution-list-hooks",
|
||||
"@npm//@babel/core",
|
||||
"@npm//babel-loader",
|
||||
"@npm//@elastic/eui",
|
||||
"@npm//react",
|
||||
"@npm//resize-observer-polyfill",
|
||||
"@npm//rxjs",
|
||||
"@npm//tslib",
|
||||
]
|
||||
|
||||
TYPES_DEPS = [
|
||||
"@npm//typescript",
|
||||
"@npm//@types/jest",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/react",
|
||||
]
|
||||
|
||||
DEPS = SRC_DEPS + TYPES_DEPS
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//:tsconfig.base.json",
|
||||
],
|
||||
)
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig_browser",
|
||||
src = "tsconfig.browser.json",
|
||||
deps = [
|
||||
"//:tsconfig.base.json",
|
||||
"//:tsconfig.browser.json",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tsc",
|
||||
args = ["--pretty"],
|
||||
srcs = SRCS,
|
||||
deps = DEPS,
|
||||
allow_js = True,
|
||||
declaration = True,
|
||||
declaration_dir = "target_types",
|
||||
declaration_map = True,
|
||||
incremental = True,
|
||||
out_dir = "target_node",
|
||||
root_dir = "src",
|
||||
source_map = True,
|
||||
tsconfig = ":tsconfig",
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tsc_browser",
|
||||
args = ['--pretty'],
|
||||
srcs = SRCS,
|
||||
deps = DEPS,
|
||||
allow_js = True,
|
||||
declaration = False,
|
||||
incremental = True,
|
||||
out_dir = "target_web",
|
||||
source_map = True,
|
||||
root_dir = "src",
|
||||
tsconfig = ":tsconfig_browser",
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = PKG_BASE_NAME,
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
srcs = NPM_MODULE_EXTRA_FILES,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [":tsc", ":tsc_browser"] + DEPS,
|
||||
)
|
||||
|
||||
pkg_npm(
|
||||
name = "npm_module",
|
||||
deps = [
|
||||
":%s" % PKG_BASE_NAME,
|
||||
]
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build",
|
||||
srcs = [
|
||||
":npm_module",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
19
packages/kbn-securitysolution-autocomplete/babel.config.js
Normal file
19
packages/kbn-securitysolution-autocomplete/babel.config.js
Normal file
|
@ -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'],
|
||||
};
|
13
packages/kbn-securitysolution-autocomplete/jest.config.js
Normal file
13
packages/kbn-securitysolution-autocomplete/jest.config.js
Normal file
|
@ -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: ['<rootDir>/packages/kbn-securitysolution-autocomplete'],
|
||||
};
|
10
packages/kbn-securitysolution-autocomplete/package.json
Normal file
10
packages/kbn-securitysolution-autocomplete/package.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "@kbn/securitysolution-autocomplete",
|
||||
"version": "1.0.0",
|
||||
"description": "Security Solution auto complete",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"browser": "./target_web/index.js",
|
||||
"main": "./target_node/index.js",
|
||||
"types": "./target_types/index.d.ts",
|
||||
"private": true
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"browser": "../target_web/react",
|
||||
"main": "../target_node/react",
|
||||
"types": "../target_types/react/index.d.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(),
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { checkEmptyValue } from '.';
|
||||
import { getField } from '../fields/index.mock';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
describe('check_empty_value', () => {
|
||||
test('returns no errors if no field has been selected', () => {
|
||||
const isValid = checkEmptyValue('', undefined, true, false);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns error string if user has touched a required input and left empty', () => {
|
||||
const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true);
|
||||
|
||||
expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR);
|
||||
});
|
||||
|
||||
test('returns no errors if required input is empty but user has not yet touched it', () => {
|
||||
const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if user has touched an input that is not required and left empty', () => {
|
||||
const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if user has touched an input that is not required and left empty string', () => {
|
||||
const isValid = checkEmptyValue('', getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns null if input value is not empty string or undefined', () => {
|
||||
const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731
|
||||
// import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
type IFieldType = any;
|
||||
|
||||
/**
|
||||
* Determines if empty value is ok
|
||||
*/
|
||||
export const checkEmptyValue = (
|
||||
param: string | undefined,
|
||||
field: IFieldType | undefined,
|
||||
isRequired: boolean,
|
||||
touched: boolean
|
||||
): string | undefined | null => {
|
||||
if (isRequired && touched && (param == null || param.trim() === '')) {
|
||||
return i18n.FIELD_REQUIRED_ERR;
|
||||
}
|
||||
|
||||
if (
|
||||
field == null ||
|
||||
(isRequired && !touched) ||
|
||||
(!isRequired && (param == null || param === ''))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -1,22 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { FieldComponent } from '.';
|
||||
import { fields, getField } from '../fields/index.mock';
|
||||
|
||||
import {
|
||||
fields,
|
||||
getField,
|
||||
} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
|
||||
import { FieldComponent } from './field';
|
||||
|
||||
describe('FieldComponent', () => {
|
||||
describe('field', () => {
|
||||
test('it renders disabled if "isDisabled" is true', () => {
|
||||
const wrapper = mount(
|
||||
<FieldComponent
|
|
@ -1,17 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731
|
||||
// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
type IFieldType = any;
|
||||
type IIndexPattern = any;
|
||||
|
||||
import { getGenericComboBoxProps } from './helpers';
|
||||
import { GetGenericComboBoxPropsReturn } from './types';
|
||||
import {
|
||||
getGenericComboBoxProps,
|
||||
GetGenericComboBoxPropsReturn,
|
||||
} from '../get_generic_combo_box_props';
|
||||
|
||||
const AS_PLAIN_TEXT = { asPlainText: true };
|
||||
|
||||
|
@ -28,13 +34,6 @@ interface OperatorProps {
|
|||
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<OperatorProps> = ({
|
||||
fieldInputWidth,
|
||||
fieldTypeFilter = [],
|
|
@ -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', () => {
|
|
@ -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';
|
|
@ -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 { getField } from '../fields/index.mock';
|
||||
import { AutocompleteFieldListsComponent } from '.';
|
||||
import {
|
||||
getListResponseMock,
|
||||
getFoundListSchemaMock,
|
||||
DATE_NOW,
|
||||
IMMUTABLE,
|
||||
VERSION,
|
||||
} from '../list_schema/index.mock';
|
||||
|
||||
import { AutocompleteFieldListsComponent } from './field_value_lists';
|
||||
|
||||
const mockKibanaHttpService = coreMock.createStart().http;
|
||||
// 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,
|
|
@ -1,20 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useFindLists } from '@kbn/securitysolution-list-hooks';
|
||||
|
||||
import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
import { filterFieldToList } from '../filter_field_to_list';
|
||||
import { getGenericComboBoxProps } from '../get_generic_combo_box_props';
|
||||
|
||||
import { filterFieldToList, getGenericComboBoxProps } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731
|
||||
// import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
type IFieldType = any;
|
||||
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715
|
||||
// import { HttpStart } from 'kibana/public';
|
||||
type HttpStart = any;
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
const SINGLE_SELECTION = { asPlainText: true };
|
||||
|
|
@ -1,27 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ReactWrapper, mount } from 'enzyme';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiSuperSelect } from '@elastic/eui';
|
||||
import { act } from '@testing-library/react';
|
||||
import { AutocompleteFieldMatchComponent } from '.';
|
||||
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
|
||||
import { fields, getField } from '../fields/index.mock';
|
||||
import { autocompleteStartMock } from '../autocomplete/index.mock';
|
||||
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import {
|
||||
fields,
|
||||
getField,
|
||||
} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
|
||||
import { AutocompleteFieldMatchComponent } from './field_value_match';
|
||||
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
|
||||
|
||||
jest.mock('./hooks/use_field_value_autocomplete');
|
||||
|
||||
const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract();
|
||||
jest.mock('../hooks/use_field_value_autocomplete');
|
||||
|
||||
describe('AutocompleteFieldMatchComponent', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
@ -299,7 +293,6 @@ describe('AutocompleteFieldMatchComponent', () => {
|
|||
selectedValue=""
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').exists()
|
||||
).toBeTruthy();
|
||||
|
@ -431,7 +424,6 @@ describe('AutocompleteFieldMatchComponent', () => {
|
|||
selectedValue=""
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="valueAutocompleteFieldMatchNumber"] input')
|
||||
.at(0)
|
|
@ -1,28 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFieldNumber,
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiFormRow,
|
||||
EuiFieldNumber,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiComboBox,
|
||||
} from '@elastic/eui';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715
|
||||
// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
|
||||
type AutocompleteStart = any;
|
||||
|
||||
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
|
||||
import { getGenericComboBoxProps, paramIsValid } from './helpers';
|
||||
import { GetGenericComboBoxPropsReturn } from './types';
|
||||
import * as i18n from './translations';
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731
|
||||
// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
type IFieldType = any;
|
||||
type IIndexPattern = any;
|
||||
|
||||
import * as i18n from '../translations';
|
||||
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
|
||||
import {
|
||||
getGenericComboBoxProps,
|
||||
GetGenericComboBoxPropsReturn,
|
||||
} from '../get_generic_combo_box_props';
|
||||
import { paramIsValid } from '../param_is_valid';
|
||||
|
||||
const BOOLEAN_OPTIONS = [
|
||||
{ inputDisplay: 'true', value: 'true' },
|
||||
|
@ -47,11 +58,6 @@ interface AutocompleteFieldMatchProps {
|
|||
onError?: (arg: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is a copy of this within:
|
||||
* x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx
|
||||
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
|
||||
*/
|
||||
export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchProps> = ({
|
||||
placeholder,
|
||||
rowLabel,
|
||||
|
@ -189,11 +195,6 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
|||
isLoadingSuggestions,
|
||||
]);
|
||||
|
||||
const fieldInputWidths = useMemo(
|
||||
() => (fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}),
|
||||
[fieldInputWidth]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
setError(undefined);
|
||||
if (onError != null) {
|
||||
|
@ -225,7 +226,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
|||
onBlur={setIsTouchedValue}
|
||||
sortMatchesBy="startsWith"
|
||||
data-test-subj="valuesAutocompleteMatch"
|
||||
style={fieldInputWidths}
|
||||
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
|
||||
fullWidth
|
||||
async
|
||||
/>
|
||||
|
@ -234,7 +235,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
|||
}, [
|
||||
comboOptions,
|
||||
error,
|
||||
fieldInputWidths,
|
||||
fieldInputWidth,
|
||||
handleCreateOption,
|
||||
handleSearchChange,
|
||||
handleValuesChange,
|
||||
|
@ -269,7 +270,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
|||
}
|
||||
onChange={handleNonComboBoxInputChange}
|
||||
data-test-subj="valueAutocompleteFieldMatchNumber"
|
||||
style={fieldInputWidths}
|
||||
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -289,7 +290,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
|||
valueOfSelected={selectedValue ?? 'true'}
|
||||
onChange={handleBooleanInputChange}
|
||||
data-test-subj="valuesAutocompleteMatchBoolean"
|
||||
style={fieldInputWidths}
|
||||
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
|
@ -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;
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
@ -10,13 +11,22 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
|
|||
import { uniq } from 'lodash';
|
||||
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715
|
||||
// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
|
||||
type AutocompleteStart = any;
|
||||
|
||||
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
|
||||
import { getGenericComboBoxProps, paramIsValid } from './helpers';
|
||||
import { GetGenericComboBoxPropsReturn } from './types';
|
||||
import * as i18n from './translations';
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731
|
||||
// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
type IFieldType = any;
|
||||
type IIndexPattern = any;
|
||||
|
||||
import * as i18n from '../translations';
|
||||
import {
|
||||
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;
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 [];
|
||||
}
|
||||
};
|
|
@ -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<string>({
|
||||
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<string>({
|
||||
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<string>({
|
||||
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<string>({
|
||||
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',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
export interface GetGenericComboBoxPropsReturn {
|
||||
comboOptions: EuiComboBoxOptionOption[];
|
||||
labels: string[];
|
||||
selectedComboOptions: EuiComboBoxOptionOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the options, selected values and option labels for EUI combo box
|
||||
* @param options options user can select from
|
||||
* @param selectedOptions user selection if any
|
||||
* @param getLabel helper function to know which property to use for labels
|
||||
*/
|
||||
export const getGenericComboBoxProps = <T>({
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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';
|
|
@ -1,28 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub';
|
||||
import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
|
||||
|
||||
import {
|
||||
UseFieldValueAutocompleteProps,
|
||||
UseFieldValueAutocompleteReturn,
|
||||
useFieldValueAutocomplete,
|
||||
} from './use_field_value_autocomplete';
|
||||
} from '.';
|
||||
import { getField } from '../../fields/index.mock';
|
||||
import { autocompleteStartMock } from '../../autocomplete/index.mock';
|
||||
|
||||
const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract();
|
||||
// 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
jest.mock('../../../../../../../../src/plugins/kibana_react/public');
|
||||
|
||||
describe('useFieldValueAutocomplete', () => {
|
||||
describe('use_field_value_autocomplete', () => {
|
||||
const onErrorMock = jest.fn();
|
||||
const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']);
|
||||
|
|
@ -1,16 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public';
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715
|
||||
// import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public';
|
||||
type AutocompleteStart = any;
|
||||
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731
|
||||
// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
type IFieldType = any;
|
||||
type IIndexPattern = any;
|
||||
|
||||
interface FuncArgs {
|
||||
fieldSelected: IFieldType | undefined;
|
||||
|
@ -33,10 +40,6 @@ export interface UseFieldValueAutocompleteProps {
|
|||
}
|
||||
/**
|
||||
* Hook for using the field value autocomplete service
|
||||
* There is a copy within:
|
||||
* x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts
|
||||
*
|
||||
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
|
||||
*/
|
||||
export const useFieldValueAutocomplete = ({
|
||||
selectedField,
|
19
packages/kbn-securitysolution-autocomplete/src/index.ts
Normal file
19
packages/kbn-securitysolution-autocomplete/src/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
export * from './check_empty_value';
|
||||
export * from './field';
|
||||
export * from './field_value_exists';
|
||||
export * from './field_value_lists';
|
||||
export * from './field_value_match';
|
||||
export * from './field_value_match_any';
|
||||
export * from './filter_field_to_list';
|
||||
export * from './get_generic_combo_box_props';
|
||||
export * from './get_operators';
|
||||
export * from './hooks';
|
||||
export * from './operator';
|
||||
export * from './param_is_valid';
|
|
@ -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,
|
||||
});
|
|
@ -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(
|
||||
<OperatorComponent
|
|
@ -1,18 +1,24 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { OperatorOption } from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731
|
||||
// import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
type IFieldType = any;
|
||||
|
||||
import { getGenericComboBoxProps, getOperators } from './helpers';
|
||||
import { GetGenericComboBoxPropsReturn } from './types';
|
||||
import { getOperators } from '../get_operators';
|
||||
import {
|
||||
getGenericComboBoxProps,
|
||||
GetGenericComboBoxPropsReturn,
|
||||
} from '../get_generic_combo_box_props';
|
||||
|
||||
const AS_PLAIN_TEXT = { asPlainText: true };
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 { paramIsValid } from '.';
|
||||
import { getField } from '../fields/index.mock';
|
||||
import * as i18n from '../translations';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('params_is_valid', () => {
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { checkEmptyValue } from '../check_empty_value';
|
||||
|
||||
// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731
|
||||
// import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
type IFieldType = any;
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
/**
|
||||
* Very basic validation for values
|
||||
* @param param the value being checked
|
||||
* @param field the selected field
|
||||
* @param isRequired whether or not an empty value is allowed
|
||||
* @param touched has field been touched by user
|
||||
* @returns undefined if valid, string with error message if invalid
|
||||
*/
|
||||
export const paramIsValid = (
|
||||
param: string | undefined,
|
||||
field: IFieldType | undefined,
|
||||
isRequired: boolean,
|
||||
touched: boolean
|
||||
): string | undefined => {
|
||||
if (field == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const emptyValueError = checkEmptyValue(param, field, isRequired, touched);
|
||||
if (emptyValueError !== null) {
|
||||
return emptyValueError;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'date':
|
||||
const moment = dateMath.parse(param ?? '');
|
||||
const isDate = Boolean(moment && moment.isValid());
|
||||
return isDate ? undefined : i18n.DATE_ERR;
|
||||
case 'number':
|
||||
const isNum = param != null && param.trim() !== '' && !isNaN(+param);
|
||||
return isNum ? undefined : i18n.NUMBER_ERR;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LOADING = i18n.translate('autocomplete.loadingDescription', {
|
||||
defaultMessage: 'Loading...',
|
||||
});
|
||||
|
||||
export const SELECT_FIELD_FIRST = i18n.translate('autocomplete.selectField', {
|
||||
defaultMessage: 'Please select a field first...',
|
||||
});
|
||||
|
||||
export const FIELD_REQUIRED_ERR = i18n.translate('autocomplete.fieldRequiredError', {
|
||||
defaultMessage: 'Value cannot be empty',
|
||||
});
|
||||
|
||||
export const NUMBER_ERR = i18n.translate('autocomplete.invalidNumberError', {
|
||||
defaultMessage: 'Not a valid number',
|
||||
});
|
||||
|
||||
export const DATE_ERR = i18n.translate('autocomplete.invalidDateError', {
|
||||
defaultMessage: 'Not a valid date',
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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')
|
||||
);
|
||||
};
|
|
@ -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__/**/*"
|
||||
]
|
||||
}
|
16
packages/kbn-securitysolution-autocomplete/tsconfig.json
Normal file
16
packages/kbn-securitysolution-autocomplete/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"incremental": true,
|
||||
"declarationDir": "./target_types",
|
||||
"outDir": "target_node",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"sourceRoot": "../../../../packages/kbn-securitysolution-autocomplete/src",
|
||||
"rootDir": "src",
|
||||
"types": ["jest", "node", "resize-observer-polyfill"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
|
@ -1,388 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import {
|
||||
EXCEPTION_OPERATORS,
|
||||
doesNotExistOperator,
|
||||
existsOperator,
|
||||
isNotOperator,
|
||||
isOperator,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import {
|
||||
checkEmptyValue,
|
||||
filterFieldToList,
|
||||
getGenericComboBoxProps,
|
||||
getOperators,
|
||||
paramIsValid,
|
||||
typeMatch,
|
||||
} from './helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
// @ts-ignore
|
||||
moment.suppressDeprecationWarnings = true;
|
||||
describe('#getOperators', () => {
|
||||
test('it returns "isOperator" if passed in field is "undefined"', () => {
|
||||
const operator = getOperators(undefined);
|
||||
|
||||
expect(operator).toEqual([isOperator]);
|
||||
});
|
||||
|
||||
test('it returns expected operators when field type is "boolean"', () => {
|
||||
const operator = getOperators(getField('ssl'));
|
||||
|
||||
expect(operator).toEqual([isOperator, isNotOperator, existsOperator, doesNotExistOperator]);
|
||||
});
|
||||
|
||||
test('it returns "isOperator" when field type is "nested"', () => {
|
||||
const operator = getOperators({
|
||||
aggregatable: false,
|
||||
count: 0,
|
||||
esTypes: ['text'],
|
||||
name: 'nestedField',
|
||||
readFromDocValues: false,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
subType: { nested: { path: 'nestedField' } },
|
||||
type: 'nested',
|
||||
});
|
||||
|
||||
expect(operator).toEqual([isOperator]);
|
||||
});
|
||||
|
||||
test('it returns all operator types when field type is not null, boolean, or nested', () => {
|
||||
const operator = getOperators(getField('machine.os.raw'));
|
||||
|
||||
expect(operator).toEqual(EXCEPTION_OPERATORS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#checkEmptyValue', () => {
|
||||
test('returns no errors if no field has been selected', () => {
|
||||
const isValid = checkEmptyValue('', undefined, true, false);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns error string if user has touched a required input and left empty', () => {
|
||||
const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true);
|
||||
|
||||
expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR);
|
||||
});
|
||||
|
||||
test('returns no errors if required input is empty but user has not yet touched it', () => {
|
||||
const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if user has touched an input that is not required and left empty', () => {
|
||||
const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if user has touched an input that is not required and left empty string', () => {
|
||||
const isValid = checkEmptyValue('', getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns null if input value is not empty string or undefined', () => {
|
||||
const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#paramIsValid', () => {
|
||||
test('returns no errors if no field has been selected', () => {
|
||||
const isValid = paramIsValid('', undefined, true, false);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns error string if user has touched a required input and left empty', () => {
|
||||
const isValid = paramIsValid(undefined, getField('@timestamp'), true, true);
|
||||
|
||||
expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR);
|
||||
});
|
||||
|
||||
test('returns no errors if required input is empty but user has not yet touched it', () => {
|
||||
const isValid = paramIsValid(undefined, getField('@timestamp'), true, false);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if user has touched an input that is not required and left empty', () => {
|
||||
const isValid = paramIsValid(undefined, getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if user has touched an input that is not required and left empty string', () => {
|
||||
const isValid = paramIsValid('', getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if field is of type date and value is valid', () => {
|
||||
const isValid = paramIsValid(
|
||||
'1994-11-05T08:15:30-05:00',
|
||||
getField('@timestamp'),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns errors if filed is of type date and value is not valid', () => {
|
||||
const isValid = paramIsValid('1593478826', getField('@timestamp'), false, true);
|
||||
|
||||
expect(isValid).toEqual(i18n.DATE_ERR);
|
||||
});
|
||||
|
||||
test('returns no errors if field is of type number and value is an integer', () => {
|
||||
const isValid = paramIsValid('4', getField('bytes'), true, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if field is of type number and value is a float', () => {
|
||||
const isValid = paramIsValid('4.3', getField('bytes'), true, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns no errors if field is of type number and value is a long', () => {
|
||||
const isValid = paramIsValid('-9223372036854775808', getField('bytes'), true, true);
|
||||
|
||||
expect(isValid).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns errors if field is of type number and value is "hello"', () => {
|
||||
const isValid = paramIsValid('hello', getField('bytes'), true, true);
|
||||
|
||||
expect(isValid).toEqual(i18n.NUMBER_ERR);
|
||||
});
|
||||
|
||||
test('returns errors if field is of type number and value is "123abc"', () => {
|
||||
const isValid = paramIsValid('123abc', getField('bytes'), true, true);
|
||||
|
||||
expect(isValid).toEqual(i18n.NUMBER_ERR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getGenericComboBoxProps', () => {
|
||||
test('it returns empty arrays if "options" is empty array', () => {
|
||||
const result = getGenericComboBoxProps<string>({
|
||||
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<string>({
|
||||
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<string>({
|
||||
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<string>({
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,183 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import type { ListSchema, Type } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import {
|
||||
EXCEPTION_OPERATORS,
|
||||
OperatorOption,
|
||||
doesNotExistOperator,
|
||||
existsOperator,
|
||||
isNotOperator,
|
||||
isOperator,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
|
||||
import { GetGenericComboBoxPropsReturn } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
/**
|
||||
* Returns the appropriate operators given a field type
|
||||
*
|
||||
* @param field IFieldType selected field
|
||||
*
|
||||
*/
|
||||
export const getOperators = (field: IFieldType | undefined): OperatorOption[] => {
|
||||
if (field == null) {
|
||||
return [isOperator];
|
||||
} else if (field.type === 'boolean') {
|
||||
return [isOperator, isNotOperator, existsOperator, doesNotExistOperator];
|
||||
} else if (field.type === 'nested') {
|
||||
return [isOperator];
|
||||
} else {
|
||||
return EXCEPTION_OPERATORS;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if empty value is ok
|
||||
*
|
||||
* @param param the value being checked
|
||||
* @param field the selected field
|
||||
* @param isRequired whether or not an empty value is allowed
|
||||
* @param touched has field been touched by user
|
||||
* @returns undefined if valid, string with error message if invalid,
|
||||
* null if no checks matched
|
||||
*/
|
||||
export const checkEmptyValue = (
|
||||
param: string | undefined,
|
||||
field: IFieldType | undefined,
|
||||
isRequired: boolean,
|
||||
touched: boolean
|
||||
): string | undefined | null => {
|
||||
if (isRequired && touched && (param == null || param.trim() === '')) {
|
||||
return i18n.FIELD_REQUIRED_ERR;
|
||||
}
|
||||
|
||||
if (
|
||||
field == null ||
|
||||
(isRequired && !touched) ||
|
||||
(!isRequired && (param == null || param === ''))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Very basic validation for values
|
||||
* There is a copy within:
|
||||
* x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
|
||||
*
|
||||
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
|
||||
*
|
||||
* @param param the value being checked
|
||||
* @param field the selected field
|
||||
* @param isRequired whether or not an empty value is allowed
|
||||
* @param touched has field been touched by user
|
||||
* @returns undefined if valid, string with error message if invalid
|
||||
*/
|
||||
export const paramIsValid = (
|
||||
param: string | undefined,
|
||||
field: IFieldType | undefined,
|
||||
isRequired: boolean,
|
||||
touched: boolean
|
||||
): string | undefined => {
|
||||
if (field == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const emptyValueError = checkEmptyValue(param, field, isRequired, touched);
|
||||
if (emptyValueError !== null) {
|
||||
return emptyValueError;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'date':
|
||||
const moment = dateMath.parse(param ?? '');
|
||||
const isDate = Boolean(moment && moment.isValid());
|
||||
return isDate ? undefined : i18n.DATE_ERR;
|
||||
case 'number':
|
||||
const isNum = param != null && param.trim() !== '' && !isNaN(+param);
|
||||
return isNum ? undefined : i18n.NUMBER_ERR;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the options, selected values and option labels for EUI combo box
|
||||
* There is a copy within:
|
||||
* x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
|
||||
*
|
||||
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
|
||||
* @param options options user can select from
|
||||
* @param selectedOptions user selection if any
|
||||
* @param getLabel helper function to know which property to use for labels
|
||||
*/
|
||||
export const getGenericComboBoxProps = <T>({
|
||||
getLabel,
|
||||
options,
|
||||
selectedOptions,
|
||||
}: {
|
||||
getLabel: (value: T) => string;
|
||||
options: T[];
|
||||
selectedOptions: T[];
|
||||
}): GetGenericComboBoxPropsReturn => {
|
||||
const newLabels = options.map(getLabel);
|
||||
const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label }));
|
||||
const newSelectedComboOptions = selectedOptions
|
||||
.map(getLabel)
|
||||
.filter((option) => {
|
||||
return newLabels.indexOf(option) !== -1;
|
||||
})
|
||||
.map((option) => {
|
||||
return newComboOptions[newLabels.indexOf(option)];
|
||||
});
|
||||
|
||||
return {
|
||||
comboOptions: newComboOptions,
|
||||
labels: newLabels,
|
||||
selectedComboOptions: newSelectedComboOptions,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an array of lists and optionally a field this will return all
|
||||
* the lists that match against the field based on the types from the field
|
||||
* @param lists The lists to match against the field
|
||||
* @param field The field to check against the list to see if they are compatible
|
||||
*/
|
||||
export const filterFieldToList = (lists: ListSchema[], field?: IFieldType): ListSchema[] => {
|
||||
if (field != null) {
|
||||
const { esTypes = [] } = field;
|
||||
return lists.filter(({ type }) => esTypes.some((esType) => typeMatch(type, esType)));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an input list type and a string based ES type this will match
|
||||
* if they're exact or if they are compatible with a range
|
||||
* @param type The type to match against the esType
|
||||
* @param esType The ES type to match with
|
||||
*/
|
||||
export const typeMatch = (type: Type, esType: string): boolean => {
|
||||
return (
|
||||
type === esType ||
|
||||
(type === 'ip_range' && esType === 'ip') ||
|
||||
(type === 'date_range' && esType === 'date') ||
|
||||
(type === 'double_range' && esType === 'double') ||
|
||||
(type === 'float_range' && esType === 'float') ||
|
||||
(type === 'integer_range' && esType === 'integer') ||
|
||||
(type === 'long_range' && esType === 'long')
|
||||
);
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { AutocompleteFieldExistsComponent } from './field_value_exists';
|
||||
export { AutocompleteFieldListsComponent } from './field_value_lists';
|
||||
export { AutocompleteFieldMatchAnyComponent } from './field_value_match_any';
|
||||
export { AutocompleteFieldMatchComponent } from './field_value_match';
|
||||
export { FieldComponent } from './field';
|
||||
export { OperatorComponent } from './operator';
|
|
@ -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',
|
||||
});
|
|
@ -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[];
|
||||
}
|
|
@ -27,16 +27,18 @@ import {
|
|||
getFilteredIndexPatterns,
|
||||
getOperatorOptions,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
import {
|
||||
AutocompleteFieldExistsComponent,
|
||||
AutocompleteFieldListsComponent,
|
||||
AutocompleteFieldMatchAnyComponent,
|
||||
AutocompleteFieldMatchComponent,
|
||||
FieldComponent,
|
||||
OperatorComponent,
|
||||
} from '@kbn/securitysolution-autocomplete';
|
||||
|
||||
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { HttpStart } from '../../../../../../../src/core/public';
|
||||
import { FieldComponent } from '../autocomplete/field';
|
||||
import { OperatorComponent } from '../autocomplete/operator';
|
||||
import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_exists';
|
||||
import { AutocompleteFieldMatchComponent } from '../autocomplete/field_value_match';
|
||||
import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any';
|
||||
import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists';
|
||||
import { getEmptyValue } from '../../../common/empty_value';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
|
|
@ -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(
|
||||
<FieldComponent
|
||||
placeholder="Placeholder text"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
selectedField={getField('machine.os.raw')}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={true}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] input`).prop('disabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders loading if "isLoading" is true', () => {
|
||||
const wrapper = mount(
|
||||
<FieldComponent
|
||||
placeholder="Placeholder text"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
selectedField={getField('machine.os.raw')}
|
||||
isLoading={true}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<FieldComponent
|
||||
placeholder="Placeholder text"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
selectedField={getField('machine.os.raw')}
|
||||
isLoading={false}
|
||||
isClearable={true}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="comboBoxInput"]`)
|
||||
.hasClass('euiComboBox__inputWrap-isClearable')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it correctly displays selected field', () => {
|
||||
const wrapper = mount(
|
||||
<FieldComponent
|
||||
placeholder="Placeholder text"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
selectedField={getField('machine.os.raw')}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<FieldComponent
|
||||
placeholder="Placeholder text"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
selectedField={getField('machine.os.raw')}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
((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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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<OperatorProps> = ({
|
||||
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 (
|
||||
<EuiComboBox
|
||||
placeholder={placeholder}
|
||||
options={comboOptions}
|
||||
selectedOptions={selectedComboOptions}
|
||||
onChange={handleValuesChange}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isDisabled}
|
||||
isClearable={isClearable}
|
||||
isInvalid={isRequired ? touched && selectedField == null : false}
|
||||
onFocus={handleTouch}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
data-test-subj="fieldAutocompleteComboBox"
|
||||
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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<IFieldType>({
|
||||
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<string, IFieldType>();
|
||||
|
||||
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;
|
||||
};
|
|
@ -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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
rowLabel={'Row Label'}
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ip')}
|
||||
selectedValue="126.45.211.34"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="valuesAutocompleteMatchLabel"] label').at(0).text()
|
||||
).toEqual('Row Label');
|
||||
});
|
||||
|
||||
test('it renders disabled if "isDisabled" is true', () => {
|
||||
wrapper = mount(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ip')}
|
||||
selectedValue="126.45.211.34"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled
|
||||
onChange={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="valuesAutocompleteMatch"] input').prop('disabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders loading if "isLoading" is true', () => {
|
||||
wrapper = mount(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ip')}
|
||||
selectedValue="126.45.211.34"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ip')}
|
||||
selectedValue="126.45.211.34"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={true}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="comboBoxInput"]')
|
||||
.hasClass('euiComboBox__inputWrap-isClearable')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it correctly displays selected value', () => {
|
||||
wrapper = mount(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ip')}
|
||||
selectedValue="126.45.211.34"
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ip')}
|
||||
selectedValue=""
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={mockOnChange}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
((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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('machine.os.raw')}
|
||||
selectedValue=""
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={mockOnChange}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
((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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('machine.os.raw')}
|
||||
selectedValue=""
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ssl')}
|
||||
selectedValue=""
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ssl')}
|
||||
selectedValue=""
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={mockOnChange}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
((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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('ssl')}
|
||||
selectedValue=""
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={mockOnChange}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
((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(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('bytes')}
|
||||
selectedValue=""
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="valueAutocompleteFieldMatchNumber"]').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it invokes "onChange" with numeric value when inputted', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
wrapper = mount(
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder="Placeholder text"
|
||||
selectedField={getField('bytes')}
|
||||
selectedValue=""
|
||||
indexPattern={{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={mockOnChange}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="valueAutocompleteFieldMatchNumber"] input')
|
||||
.at(0)
|
||||
.simulate('change', { target: { value: '8' } });
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('8');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<AutocompleteFieldMatchProps> = ({
|
||||
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<string | undefined>(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<string>({
|
||||
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<HTMLInputElement>): 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 (
|
||||
<EuiFormRow
|
||||
label={rowLabel}
|
||||
error={error}
|
||||
isInvalid={selectedField != null && error != null}
|
||||
data-test-subj="valuesAutocompleteMatchLabel"
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
placeholder={inputPlaceholder}
|
||||
isDisabled={isDisabled || !selectedField}
|
||||
isLoading={isLoadingState}
|
||||
isClearable={isClearable}
|
||||
options={comboOptions}
|
||||
selectedOptions={selectedComboOptions}
|
||||
onChange={handleValuesChange}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
onSearchChange={handleSearchChange}
|
||||
onCreateOption={handleCreateOption}
|
||||
isInvalid={selectedField != null && error != null}
|
||||
onBlur={setIsTouchedValue}
|
||||
sortMatchesBy="startsWith"
|
||||
data-test-subj="valuesAutocompleteMatch"
|
||||
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
|
||||
fullWidth
|
||||
async
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}, [
|
||||
comboOptions,
|
||||
error,
|
||||
fieldInputWidth,
|
||||
handleCreateOption,
|
||||
handleSearchChange,
|
||||
handleValuesChange,
|
||||
inputPlaceholder,
|
||||
isClearable,
|
||||
isDisabled,
|
||||
isLoadingState,
|
||||
rowLabel,
|
||||
selectedComboOptions,
|
||||
selectedField,
|
||||
setIsTouchedValue,
|
||||
]);
|
||||
|
||||
if (!isSuggestingValues && selectedField != null) {
|
||||
switch (selectedField.type) {
|
||||
case 'number':
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={rowLabel}
|
||||
error={error}
|
||||
isInvalid={selectedField != null && error != null}
|
||||
data-test-subj="valuesAutocompleteMatchLabel"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldNumber
|
||||
placeholder={inputPlaceholder}
|
||||
onBlur={setIsTouchedValue}
|
||||
value={
|
||||
typeof selectedValue === 'string' && selectedValue.trim().length > 0
|
||||
? parseFloat(selectedValue)
|
||||
: selectedValue ?? ''
|
||||
}
|
||||
onChange={handleNonComboBoxInputChange}
|
||||
data-test-subj="valueAutocompleteFieldMatchNumber"
|
||||
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={rowLabel}
|
||||
error={error}
|
||||
isInvalid={selectedField != null && error != null}
|
||||
data-test-subj="valuesAutocompleteMatchLabel"
|
||||
fullWidth
|
||||
>
|
||||
<EuiSuperSelect
|
||||
isLoading={isLoadingState}
|
||||
options={[
|
||||
{ value: 'true', inputDisplay: 'true' },
|
||||
{ value: 'false', inputDisplay: 'false' },
|
||||
]}
|
||||
valueOfSelected={selectedValue ?? 'true'}
|
||||
onChange={handleBooleanInputChange}
|
||||
data-test-subj="valuesAutocompleteMatchBoolean"
|
||||
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
default:
|
||||
return defaultInput;
|
||||
}
|
||||
} else {
|
||||
return defaultInput;
|
||||
}
|
||||
};
|
||||
|
||||
AutocompleteFieldMatchComponent.displayName = 'AutocompleteFieldMatch';
|
|
@ -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<string>({
|
||||
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<string>({
|
||||
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<string>({
|
||||
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<string>({
|
||||
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',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<T>({
|
||||
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,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<string[]>([]);
|
||||
const updateSuggestions = useRef<Func | null>(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];
|
||||
};
|
|
@ -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
|
||||
<FieldComponent
|
||||
placeholder={i18n.FIELD_PLACEHOLDER}
|
||||
indexPattern={indexPattern}
|
||||
selectedField={selectedField}
|
||||
isLoading={isLoading}
|
||||
isClearable={isClearable}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
```
|
||||
|
||||
## 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
|
||||
<OperatorComponent
|
||||
placeholder={i18n.OPERATOR_PLACEHOLDER}
|
||||
selectedField={selectedField}
|
||||
operator={selectedOperator}
|
||||
isDisabled={iDisabled}
|
||||
isLoading={isLoading}
|
||||
isClearable={isClearable}
|
||||
onChange={handleOperatorChange}
|
||||
/>
|
||||
```
|
||||
|
||||
## 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
|
||||
<AutocompleteFieldExistsComponent placeholder={i18n.EXISTS_VALUE_PLACEHOLDER} />
|
||||
```
|
||||
|
||||
## 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
|
||||
<AutocompleteFieldListsComponent
|
||||
selectedField={selectedField}
|
||||
placeholder={i18n.FIELD_LISTS_PLACEHOLDER}
|
||||
selectedValue={id}
|
||||
isLoading={isLoading}
|
||||
isDisabled={iDisabled}
|
||||
isClearable={isClearable}
|
||||
onChange={handleFieldListValueChange}
|
||||
/>
|
||||
```
|
||||
|
||||
## 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
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder={i18n.FIELD_VALUE_PLACEHOLDER}
|
||||
selectedField={selectedField}
|
||||
selectedValue={value}
|
||||
isDisabled={iDisabled}
|
||||
isLoading={isLoading}
|
||||
isClearable={isClearable}
|
||||
indexPattern={indexPattern}
|
||||
onChange={handleFieldMatchValueChange}
|
||||
/>
|
||||
```
|
||||
|
||||
## 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
|
||||
<AutocompleteFieldMatchAnyComponent
|
||||
placeholder={i18n.FIELD_VALUE_PLACEHOLDER}
|
||||
selectedField={selectedField}
|
||||
selectedValue={values}
|
||||
isDisabled={false}
|
||||
isLoading={isLoading}
|
||||
isClearable={false}
|
||||
indexPattern={indexPattern}
|
||||
onChange={handleFieldMatchAnyValueChange}
|
||||
/>
|
||||
```
|
|
@ -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',
|
||||
});
|
|
@ -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[];
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 = ({
|
|||
|
||||
<EuiFlexItemComboBoxColumn>
|
||||
<AutocompleteFieldMatchComponent
|
||||
autocompleteService={services.data.autocomplete}
|
||||
placeholder={''}
|
||||
selectedField={getFieldTypeByMapping(severityMappingItem, indices)}
|
||||
selectedValue={severityMappingItem.value}
|
||||
|
|
|
@ -12909,11 +12909,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": "ネストされていない条件を追加",
|
||||
|
@ -15328,7 +15323,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.deepLink.anomalyDetection": "異常検知",
|
||||
|
@ -18699,11 +18694,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": "閉じる",
|
||||
|
@ -20564,6 +20554,8 @@
|
|||
"xpack.securitySolution.open.timeline.singleTemplateLabel": "テンプレート",
|
||||
"xpack.securitySolution.open.timeline.singleTimelineLabel": "タイムライン",
|
||||
"xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "{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.successfullyExportedTimelineTemplatesTitle": "{totalTimelineTemplates, plural, =0 {すべてのタイムライン} other {{totalTimelineTemplates} タイムラインテンプレート}}が正常にエクスポートされました",
|
||||
"xpack.securitySolution.open.timeline.timelineNameTableHeader": "タイムライン名",
|
||||
"xpack.securitySolution.open.timeline.timelineTemplateNameTableHeader": "テンプレート名",
|
||||
|
@ -24097,4 +24089,4 @@
|
|||
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
|
||||
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13080,11 +13080,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": "添加非嵌套条件",
|
||||
|
@ -15545,7 +15540,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.deepLink.anomalyDetection": "异常检测",
|
||||
|
@ -18964,11 +18959,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": "关闭",
|
||||
|
|
Loading…
Reference in a new issue