[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:
Frank Hassanabad 2021-07-22 18:13:58 -06:00 committed by GitHub
parent 50b9784dd7
commit 94fc5d1139
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1521 additions and 2760 deletions

View file

@ -1,5 +1,6 @@
{
"paths": {
"autocomplete": "packages/kbn-securitysolution-autocomplete/src",
"console": "src/plugins/console",
"core": "src/core",
"discover": "src/plugins/discover",

View file

@ -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

View file

@ -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",

View file

@ -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",

View 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"],
)

View 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'],
};

View 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'],
};

View 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
}

View file

@ -0,0 +1,5 @@
{
"browser": "../target_web/react",
"main": "../target_node/react",
"types": "../target_types/react/index.d.ts"
}

View file

@ -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(),
};

View file

@ -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();
});
});

View file

@ -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;
};

View file

@ -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

View file

@ -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 = [],

View file

@ -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', () => {

View file

@ -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';

View file

@ -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,

View file

@ -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 };

View file

@ -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)

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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);
});
});

View file

@ -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 [];
}
};

View file

@ -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',
},
],
});
});
});

View file

@ -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,
};
};

View file

@ -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);
});
});

View file

@ -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;
}
};

View file

@ -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';

View file

@ -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']);

View file

@ -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,

View 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';

View file

@ -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,
});

View file

@ -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

View file

@ -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 };

View file

@ -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);
});
});

View file

@ -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;
}
};

View file

@ -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',
});

View file

@ -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);
});
});

View file

@ -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')
);
};

View file

@ -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__/**/*"
]
}

View 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/**/*"]
}

View file

@ -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);
});
});
});

View file

@ -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')
);
};

View file

@ -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';

View file

@ -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',
});

View file

@ -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[];
}

View file

@ -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';

View file

@ -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',
},
]);
});
});

View file

@ -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;
};

View file

@ -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');
});
});
});

View file

@ -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';

View file

@ -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',
},
],
});
});
});
});

View file

@ -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,
};
}

View file

@ -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);
});
});
});

View file

@ -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];
};

View file

@ -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}
/>
```

View file

@ -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',
});

View file

@ -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[];
}

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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}

View file

@ -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": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}

View file

@ -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": "关闭",

View file

@ -2851,6 +2851,10 @@
version "0.0.0"
uid ""
"@kbn/securitysolution-autocomplete@link:bazel-bin/packages/kbn-securitysolution-autocomplete":
version "0.0.0"
uid ""
"@kbn/securitysolution-es-utils@link:bazel-bin/packages/kbn-securitysolution-es-utils":
version "0.0.0"
uid ""