diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml
index 0b56007cf9aeb..5c3f6aef0271e 100644
--- a/.buildkite/ftr_configs.yml
+++ b/.buildkite/ftr_configs.yml
@@ -243,6 +243,7 @@ enabled:
- x-pack/test/functional/config_security_basic.ts
- x-pack/test/functional/config.ccs.ts
- x-pack/test/functional/config.firefox.js
+ - x-pack/test/functional_cloud/config.ts
- x-pack/test/kubernetes_security/basic/config.ts
- x-pack/test/licensing_plugin/config.public.ts
- x-pack/test/licensing_plugin/config.ts
diff --git a/docs/osquery/exported-fields-reference.asciidoc b/docs/osquery/exported-fields-reference.asciidoc
index c27b6e67a4062..fc16ec3e0d9d0 100644
--- a/docs/osquery/exported-fields-reference.asciidoc
+++ b/docs/osquery/exported-fields-reference.asciidoc
@@ -82,7 +82,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*activity* - keyword, number.long
-* _unified_log.activity_ - the activity ID associate with the entry.
+* _unified_log.activity_ - the activity ID associate with the entry
*actual* - keyword, number.long
@@ -101,7 +101,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _arp_cache.address_ - IPv4 address target
* _dns_resolvers.address_ - Resolver IP/IPv6 address
* _etc_hosts.address_ - IP address mapping
-* _fbsd_kmods.address_ - Kernel module address
* _interface_addresses.address_ - Specific address for interface
* _kernel_modules.address_ - Kernel module address
* _listening_ports.address_ - Specific address for bind
@@ -187,7 +186,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _deb_packages.arch_ - Package architecture
* _docker_version.arch_ - Hardware architecture
* _os_version.arch_ - OS Architecture
-* _pkg_packages.arch_ - Architecture(s) supported
* _rpm_packages.arch_ - Architecture(s) supported
* _seccomp_events.arch_ - Information about the CPU architecture
* _signature.arch_ - If applicable, the arch of the signed code
@@ -247,6 +245,42 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _chassis_info.audible_alarm_ - If TRUE, the frame is equipped with an audible alarm.
+*audit_account_logon* - keyword, number.long
+
+* _security_profile_info.audit_account_logon_ - Determines whether the operating system MUST audit each time this computer validates the credentials of an account
+
+*audit_account_manage* - keyword, number.long
+
+* _security_profile_info.audit_account_manage_ - Determines whether the operating system MUST audit each event of account management on a computer
+
+*audit_ds_access* - keyword, number.long
+
+* _security_profile_info.audit_ds_access_ - Determines whether the operating system MUST audit each instance of user attempts to access an Active Directory object that has its own system access control list (SACL) specified
+
+*audit_logon_events* - keyword, number.long
+
+* _security_profile_info.audit_logon_events_ - Determines whether the operating system MUST audit each instance of a user attempt to log on or log off this computer
+
+*audit_object_access* - keyword, number.long
+
+* _security_profile_info.audit_object_access_ - Determines whether the operating system MUST audit each instance of user attempts to access a non-Active Directory object that has its own SACL specified
+
+*audit_policy_change* - keyword, number.long
+
+* _security_profile_info.audit_policy_change_ - Determines whether the operating system MUST audit each instance of user attempts to change user rights assignment policy, audit policy, account policy, or trust policy
+
+*audit_privilege_use* - keyword, number.long
+
+* _security_profile_info.audit_privilege_use_ - Determines whether the operating system MUST audit each instance of user attempts to exercise a user right
+
+*audit_process_tracking* - keyword, number.long
+
+* _security_profile_info.audit_process_tracking_ - Determines whether the operating system MUST audit process-related events
+
+*audit_system_events* - keyword, number.long
+
+* _security_profile_info.audit_system_events_ - Determines whether the operating system MUST audit System Change, System Startup, System Shutdown, Authentication Component Load, and Loss or Excess of Security events
+
*auid* - keyword
* _process_events.auid_ - Audit User ID at process start
@@ -625,7 +659,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _ntfs_journal_events.category_ - The category that the event originated from
* _power_sensors.category_ - The sensor category: currents, voltage, wattage
* _system_extensions.category_ - System extension category
-* _unified_log.category_ - The category of the os_log_t used
+* _unified_log.category_ - the category of the os_log_t used
* _yara_events.category_ - The category of the file
*cdhash* - keyword, text.text
@@ -731,6 +765,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _wmi_filter_consumer_binding.class_ - The name of the class.
* _wmi_script_event_consumers.class_ - The name of the class.
+*clear_text_password* - keyword, number.long
+
+* _security_profile_info.clear_text_password_ - Determines whether passwords MUST be stored by using reversible encryption
+
*client_app_id* - keyword, text.text
* _windows_update_history.client_app_id_ - Identifier of the client application that processed an update
@@ -767,6 +805,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _os_version.codename_ - OS version codename
+*codesigning_flags* - keyword, text.text
+
+* _es_process_events.codesigning_flags_ - Codesigning flags matching one of these options, in a comma separated list: NOT_VALID, ADHOC, NOT_RUNTIME, INSTALLER. See kern/cs_blobs.h in XNU for descriptions.
+
*collect_cross_processes* - keyword, number.long
* _carbon_black_info.collect_cross_processes_ - If the sensor is configured to cross process events
@@ -848,7 +890,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _authorized_keys.comment_ - Optional comment
* _docker_image_history.comment_ - Instruction comment
* _etc_protocols.comment_ - Comment with protocol description
-* _etc_services.comment_ - Optional comment for a service
+* _etc_services.comment_ - Optional comment for a service.
* _groups.comment_ - Remarks or comments associated with the group
* _keychain_items.comment_ - Optional keychain comment
@@ -1092,7 +1134,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _docker_image_history.created_ - Time of creation as UNIX time
* _docker_images.created_ - Time of creation as UNIX time
* _docker_networks.created_ - Time of creation as UNIX time
-* _keychain_items.created_ - Data item was created
+* _keychain_items.created_ - Date item was created
*created_at* - keyword, text.text
@@ -1590,6 +1632,14 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _processes.elevated_token_ - Process uses elevated token yes=1, no=0
+*enable_admin_account* - keyword, number.long
+
+* _security_profile_info.enable_admin_account_ - Determines whether the Administrator account on the local computer is enabled
+
+*enable_guest_account* - keyword, number.long
+
+* _security_profile_info.enable_guest_account_ - Determines whether the Guest account on the local computer is enabled
+
*enable_ipv6* - keyword, number.long
* _docker_networks.enable_ipv6_ - 1 if IPv6 is enabled on this network. 0 otherwise
@@ -1949,7 +1999,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*firmware_type* - keyword, text.text
-* _platform_info.firmware_type_ - The type of firmware (Uefi, Bios, Unknown).
+* _platform_info.firmware_type_ - The type of firmware (uefi, bios, iboot, openfirmware, unknown).
*firmware_version* - keyword, text.text
@@ -1972,10 +2022,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _pipes.flags_ - The flags indicating whether this pipe connection is a server or client end, and if the pipe for sending messages or bytes
* _routes.flags_ - Flags to describe route
-*flatsize* - keyword, number.long
-
-* _pkg_packages.flatsize_ - Package size in bytes
-
*folder_id* - keyword, text.text
* _ycloud_instance_metadata.folder_id_ - Folder identifier for the VM
@@ -1984,6 +2030,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _systemd_units.following_ - The name of another unit that this unit follows in state
+*force_logoff_when_expire* - keyword, number.long
+
+* _security_profile_info.force_logoff_when_expire_ - Determines whether SMB client sessions with the SMB server will be forcibly disconnected when the client's logon hours expire
+
*forced* - keyword, number.long
* _preferences.forced_ - 1 if the value is forced/managed, else 0
@@ -2250,7 +2300,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*hostname* - keyword, text.text
-* _curl_certificate.hostname_ - Hostname to CURL (domain[:port], for example, osquery.io)
+* _curl_certificate.hostname_ - Hostname to CURL (domain[:port], e.g. osquery.io)
* _system_info.hostname_ - Network hostname including domain
* _ycloud_instance_metadata.hostname_ - Hostname of the VM
@@ -2626,7 +2676,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*is_active* - keyword, number.long
-* _running_apps.is_active_ - 1 if the application is in focus, 0 otherwise
+* _running_apps.is_active_ - (DEPRECATED)
*is_hidden* - keyword, number.long
@@ -2949,6 +2999,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _shared_memory.locked_ - 1 if segment is locked else 0
+*lockout_bad_count* - keyword, number.long
+
+* _security_profile_info.lockout_bad_count_ - Number of failed logon attempts after which a user account MUST be locked out
+
*log_file_disk_quota_mb* - keyword, number.long
* _carbon_black_info.log_file_disk_quota_mb_ - Event file disk quota in MB
@@ -2997,10 +3051,18 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _logon_sessions.logon_time_ - The time the session owner logged on.
+*logon_to_change_password* - keyword, number.long
+
+* _security_profile_info.logon_to_change_password_ - Determines if logon session is required to change the password
+
*logon_type* - keyword, text.text
* _logon_sessions.logon_type_ - The logon method.
+*lsa_anonymous_name_lookup* - keyword, number.long
+
+* _security_profile_info.lsa_anonymous_name_lookup_ - Determines if an anonymous user is allowed to query the local LSA policy
+
*mac* - keyword, text.text
* _arp_cache.mac_ - MAC address of broadcasted address
@@ -3110,7 +3172,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*max_rows* - keyword, number.long
-* _unified_log.max_rows_ - The max number of rows returned (defaults to 100).
+* _unified_log.max_rows_ - the max number of rows returned (defaults to 100)
*max_speed* - keyword, number.long
@@ -3124,6 +3186,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _shared_resources.maximum_allowed_ - Limit on the maximum number of users allowed to use this resource concurrently. The value is only valid if the AllowMaximum property is set to FALSE.
+*maximum_password_age* - keyword, number.long
+
+* _security_profile_info.maximum_password_age_ - Determines the maximum number of days that a password can be used before the client requires the user to change it
+
*md5* - keyword, text.text
* _acpi_tables.md5_ - MD5 hash of table content
@@ -3240,7 +3306,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _lxd_cluster_members.message_ - Message from the node (Online/Offline)
* _selinux_events.message_ - Message
* _syslog_events.message_ - The syslog message
-* _unified_log.message_ - Composed message
+* _unified_log.message_ - composed message
* _user_events.message_ - Message from the event
*metadata_endpoint* - keyword, text.text
@@ -3297,6 +3363,14 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _memory_devices.min_voltage_ - Minimum operating voltage of device in millivolts
+*minimum_password_age* - keyword, number.long
+
+* _security_profile_info.minimum_password_age_ - Determines the minimum number of days that a password must be used before the user can change it
+
+*minimum_password_length* - keyword, number.long
+
+* _security_profile_info.minimum_password_length_ - Determines the least number of characters that can make up a password for a user account
+
*minimum_system_version* - keyword, text.text
* _apps.minimum_system_version_ - Minimum version of macOS required for the app to run
@@ -3459,7 +3533,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _etc_protocols.name_ - Protocol name
* _etc_services.name_ - Service name
* _fan_speed_sensors.name_ - Fan name
-* _fbsd_kmods.name_ - Module name
* _firefox_addons.name_ - Addon display name
* _homebrew_packages.name_ - Package name
* _ie_extensions.name_ - Extension display name
@@ -3491,7 +3564,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _package_install_history.name_ - Package display name
* _physical_disk_performance.name_ - Name of the physical disk
* _pipes.name_ - Name of the pipe
-* _pkg_packages.name_ - Package name
* _power_sensors.name_ - Name of power source
* _processes.name_ - The process path or shorthand argv[0]
* _programs.name_ - Commonly used product name.
@@ -3529,7 +3601,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*native* - keyword, number.long
* _browser_plugins.native_ - Plugin requires native execution
-* _firefox_addons.native_ - 1 If the addon includes binary components else 0
*net_namespace* - keyword, text.text
@@ -3561,6 +3632,14 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _docker_container_stats.network_tx_bytes_ - Total network bytes transmitted
+*new_administrator_name* - keyword, text.text
+
+* _security_profile_info.new_administrator_name_ - Determines the name of the Administrator account on the local computer
+
+*new_guest_name* - keyword, text.text
+
+* _security_profile_info.new_guest_name_ - Determines the name of the Guest account on the local computer
+
*next_run_time* - keyword, number.long
* _scheduled_tasks.next_run_time_ - Timestamp the task is scheduled to run next
@@ -3916,6 +3995,14 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _wifi_networks.passpoint_ - 1 if Passpoint is supported, 0 otherwise
+*password_complexity* - keyword, number.long
+
+* _security_profile_info.password_complexity_ - Determines whether passwords must meet a series of strong-password guidelines
+
+*password_history_size* - keyword, number.long
+
+* _security_profile_info.password_history_size_ - Number of unique new passwords that must be associated with a user account before an old password can be reused
+
*password_last_set_time* - keyword, number.double
* _account_policy_data.password_last_set_time_ - The time the password was last changed
@@ -4150,10 +4237,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _processes.pid_ - Process (or thread) ID
* _running_apps.pid_ - The pid of the application
* _seccomp_events.pid_ - Process ID
-* _services.pid_ - The Process ID of the service
+* _services.pid_ - the Process ID of the service
* _shared_memory.pid_ - Process ID to last use the segment
* _socket_events.pid_ - Process (or thread) ID
-* _unified_log.pid_ - The pid of the process that made the entry
+* _unified_log.pid_ - the pid of the process that made the entry
* _user_events.pid_ - Process (or thread) ID
* _windows_crashes.pid_ - Process ID of the crashed process
* _windows_eventlog.pid_ - Process ID which emitted the event record
@@ -4327,7 +4414,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*process* - keyword, text.text
* _alf_explicit_auths.process_ - Process name explicitly allowed
-* _unified_log.process_ - The name of the process that made the entry
+* _unified_log.process_ - the name of the process that made the entry
*process_being_tapped* - keyword, number.long
@@ -4560,7 +4647,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*refs* - keyword, number.long
-* _fbsd_kmods.refs_ - Module reverse dependencies
* _kernel_extensions.refs_ - Reference count
*region* - keyword, text.text
@@ -4875,7 +4961,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*sender* - keyword, text.text
* _asl.sender_ - Sender's identification string. Default is process name.
-* _unified_log.sender_ - The name of the binary image that made the entry
+* _unified_log.sender_ - the name of the binary image that made the entry
*sensor_backend_server* - keyword, text.text
@@ -5101,7 +5187,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _device_file.size_ - Size of file in bytes
* _disk_events.size_ - Size of partition in bytes
* _docker_image_history.size_ - Size of instruction in bytes
-* _fbsd_kmods.size_ - Size of module content
* _file.size_ - Size of file in bytes
* _file_events.size_ - Size of file in bytes
* _kernel_extensions.size_ - Bytes of wired memory used by extension
@@ -5337,7 +5422,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*storage* - keyword, number.long
-* _unified_log.storage_ - The storage category for the entry.
+* _unified_log.storage_ - the storage category for the entry
*storage_driver* - keyword, text.text
@@ -5416,7 +5501,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*subsystem* - keyword, text.text
* _system_controls.subsystem_ - Subsystem ID, control type
-* _unified_log.subsystem_ - The subsystem of the os_log_t used
+* _unified_log.subsystem_ - the subsystem of the os_log_t used
*subsystem_model* - keyword, text.text
@@ -5585,7 +5670,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _bpf_process_events.tid_ - Thread ID
* _bpf_socket_events.tid_ - Thread ID
-* _unified_log.tid_ - The tid of the thread that made the entry
+* _unified_log.tid_ - the tid of the thread that made the entry
* _windows_crashes.tid_ - Thread ID of the crashed thread
* _windows_eventlog.tid_ - Thread ID which emitted the event record
@@ -5637,7 +5722,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
*timestamp* - keyword, text.text
* _time.timestamp_ - Current timestamp (log format) in UTC
-* _unified_log.timestamp_ - Unix timestamp associated with the entry
+* _unified_log.timestamp_ - unix timestamp associated with the entry
* _windows_eventlog.timestamp_ - Timestamp to selectively filter the events
*timestamp_ms* - keyword, number.long
@@ -6078,7 +6163,6 @@ For more information about osquery tables, see the https://osquery.io/schema[osq
* _osquery_packs.version_ - Minimum osquery version that this query will run on
* _package_install_history.version_ - Package display version
* _package_receipts.version_ - Installed package version
-* _pkg_packages.version_ - Package version
* _platform_info.version_ - Platform code version
* _portage_keywords.version_ - The version which are affected by the use flags, empty means all
* _portage_packages.version_ - The version which are affected by the use flags, empty means all
diff --git a/package.json b/package.json
index 5fbca346e2204..c2613ebda6e76 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,7 @@
"@dnd-kit/utilities": "^2.0.0",
"@elastic/apm-rum": "^5.12.0",
"@elastic/apm-rum-react": "^1.4.2",
- "@elastic/charts": "51.3.0",
+ "@elastic/charts": "52.0.0",
"@elastic/datemath": "5.0.3",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.6.0-canary.3",
"@elastic/ems-client": "8.4.0",
@@ -109,9 +109,11 @@
"@elastic/filesaver": "1.1.2",
"@elastic/node-crypto": "1.2.1",
"@elastic/numeral": "^2.5.1",
- "@elastic/react-search-ui": "^1.14.0",
+ "@elastic/react-search-ui": "^1.19.0",
"@elastic/request-crypto": "2.0.1",
- "@elastic/search-ui-app-search-connector": "^1.14.0",
+ "@elastic/search-ui": "^1.19.0",
+ "@elastic/search-ui-app-search-connector": "^1.19.0",
+ "@elastic/search-ui-engines-connector": "^1.19.0",
"@emotion/cache": "^11.10.3",
"@emotion/css": "^11.10.0",
"@emotion/react": "^11.10.4",
@@ -421,8 +423,8 @@
"@opentelemetry/semantic-conventions": "^1.4.0",
"@reduxjs/toolkit": "1.7.2",
"@slack/webhook": "^5.0.4",
- "@tanstack/react-query": "^4.23.0",
- "@tanstack/react-query-devtools": "^4.23.0",
+ "@tanstack/react-query": "^4.24.2",
+ "@tanstack/react-query-devtools": "^4.24.2",
"@turf/along": "6.0.1",
"@turf/area": "6.0.1",
"@turf/bbox": "6.0.1",
@@ -583,7 +585,7 @@
"react-fast-compare": "^2.0.4",
"react-focus-on": "^3.7.0",
"react-grid-layout": "^1.3.4",
- "react-hook-form": "^7.42.1",
+ "react-hook-form": "^7.43.0",
"react-intl": "^2.8.0",
"react-is": "^17.0.2",
"react-markdown": "^6.0.3",
@@ -1174,7 +1176,7 @@
"svgo": "^2.8.0",
"tape": "^5.0.1",
"tempy": "^0.3.0",
- "terser": "^5.15.1",
+ "terser": "^5.16.1",
"terser-webpack-plugin": "^4.2.3",
"tough-cookie": "^4.1.2",
"tree-kill": "^1.2.2",
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/aggs_types/bucket_aggs.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/aggs_types/bucket_aggs.ts
index 20ebeb234b479..76abc1b08bd84 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/aggs_types/bucket_aggs.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/aggs_types/bucket_aggs.ts
@@ -117,7 +117,7 @@ const termsSchema = s.object({
exclude: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])),
include: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])),
execution_hint: s.maybe(s.string()),
- missing: s.maybe(s.number()),
+ missing: s.maybe(s.oneOf([s.number(), s.string(), s.boolean()])),
min_doc_count: s.maybe(s.number({ min: 1 })),
size: s.maybe(s.number()),
show_term_doc_count_error: s.maybe(s.boolean()),
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.test.ts
index db50ab2b45d65..0120ecf75c797 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.test.ts
@@ -397,22 +397,6 @@ describe('validateAndConvertAggregations', () => {
);
});
- it('throws an error when an attributes is not respecting its schema definition', () => {
- const aggregations: AggsMap = {
- someAgg: {
- terms: {
- missing: 'expecting a number',
- },
- },
- };
-
- expect(() =>
- validateAndConvertAggregations(['alert'], aggregations, mockMappings)
- ).toThrowErrorMatchingInlineSnapshot(
- `"[someAgg.terms.missing]: expected value of type [number] but got [string]"`
- );
- });
-
it('throws an error when trying to validate an unknown aggregation type', () => {
const aggregations: AggsMap = {
someAgg: {
diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts
index 0b253cedbc672..6b30af3f39097 100644
--- a/packages/kbn-doc-links/src/get_doc_links.ts
+++ b/packages/kbn-doc-links/src/get_doc_links.ts
@@ -135,6 +135,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
crawlerOverview: `${ENTERPRISE_SEARCH_DOCS}crawler.html`,
deployTrainedModels: `${MACHINE_LEARNING_DOCS}ml-nlp-deploy-models.html`,
documentLevelSecurity: `${ELASTICSEARCH_DOCS}document-level-security.html`,
+ engines: `${ENTERPRISE_SEARCH_DOCS}engines.html`,
ingestPipelines: `${ENTERPRISE_SEARCH_DOCS}ingest-pipelines.html`,
languageAnalyzers: `${ELASTICSEARCH_DOCS}analysis-lang-analyzer.html`,
languageClients: `${ENTERPRISE_SEARCH_DOCS}programming-language-clients.html`,
diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts
index dbfb37905172f..a6284078d3d8d 100644
--- a/packages/kbn-doc-links/src/types.ts
+++ b/packages/kbn-doc-links/src/types.ts
@@ -120,6 +120,7 @@ export interface DocLinks {
readonly crawlerOverview: string;
readonly deployTrainedModels: string;
readonly documentLevelSecurity: string;
+ readonly engines: string;
readonly ingestPipelines: string;
readonly languageAnalyzers: string;
readonly languageClients: string;
diff --git a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx
index 6066d7e81a58f..2043950a61d68 100644
--- a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx
+++ b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx
@@ -27,6 +27,7 @@ export const FieldComponent: React.FC = ({
placeholder,
selectedField,
acceptsCustomOptions = false,
+ showMappingConflicts = false,
}): JSX.Element => {
const {
isInvalid,
@@ -44,6 +45,7 @@ export const FieldComponent: React.FC = ({
isRequired,
selectedField,
fieldInputWidth,
+ showMappingConflicts,
onChange,
});
@@ -68,6 +70,7 @@ export const FieldComponent: React.FC = ({
values: { searchValuePlaceholder: '{searchValue}' },
})}
fullWidth
+ renderOption={renderFields}
/>
);
}
diff --git a/packages/kbn-securitysolution-autocomplete/src/field/types.ts b/packages/kbn-securitysolution-autocomplete/src/field/types.ts
index 35de595baef68..337ded6087a59 100644
--- a/packages/kbn-securitysolution-autocomplete/src/field/types.ts
+++ b/packages/kbn-securitysolution-autocomplete/src/field/types.ts
@@ -7,6 +7,7 @@
*/
import { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
+import { FieldConflictsInfo } from '@kbn/securitysolution-list-utils';
import { GetGenericComboBoxPropsReturn } from '../get_generic_combo_box_props';
export interface FieldProps extends FieldBaseProps {
@@ -15,6 +16,7 @@ export interface FieldProps extends FieldBaseProps {
isLoading: boolean;
placeholder: string;
acceptsCustomOptions?: boolean;
+ showMappingConflicts?: boolean;
}
export interface FieldBaseProps {
indexPattern: DataViewBase | undefined;
@@ -22,6 +24,7 @@ export interface FieldBaseProps {
isRequired?: boolean;
selectedField?: DataViewFieldBase | undefined;
fieldInputWidth?: number;
+ showMappingConflicts?: boolean;
onChange: (a: DataViewFieldBase[]) => void;
}
@@ -32,6 +35,7 @@ export interface ComboBoxFields {
export interface GetFieldComboBoxPropsReturn extends GetGenericComboBoxPropsReturn {
disabledLabelTooltipTexts: { [label: string]: string };
+ mappingConflictsTooltipInfo: { [label: string]: FieldConflictsInfo[] };
}
export interface DataViewField extends DataViewFieldBase {
diff --git a/packages/kbn-securitysolution-autocomplete/src/field/use_field.tsx b/packages/kbn-securitysolution-autocomplete/src/field/use_field.tsx
index e2396b93d251d..ce2d5b987ec16 100644
--- a/packages/kbn-securitysolution-autocomplete/src/field/use_field.tsx
+++ b/packages/kbn-securitysolution-autocomplete/src/field/use_field.tsx
@@ -7,10 +7,18 @@
*/
import React from 'react';
import { useCallback, useMemo, useState } from 'react';
-import { EuiComboBoxOptionOption, EuiToolTip } from '@elastic/eui';
+import {
+ EuiComboBoxOptionOption,
+ EuiIcon,
+ EuiSpacer,
+ EuiToolTip,
+ useEuiPaddingSize,
+} from '@elastic/eui';
import { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
+import { FieldConflictsInfo, getMappingConflictsInfo } from '@kbn/securitysolution-list-utils';
import { getGenericComboBoxProps } from '../get_generic_combo_box_props';
+import * as i18n from '../translations';
import {
ComboBoxFields,
DataViewField,
@@ -72,6 +80,22 @@ const getDisabledLabelTooltipTexts = (fields: ComboBoxFields) => {
);
return disabledLabelTooltipTexts;
};
+
+const getMappingConflictsTooltipInfo = (fields: ComboBoxFields) => {
+ const mappingConflictsTooltipInfo = fields.availableFields.reduce(
+ (acc: { [label: string]: FieldConflictsInfo[] }, field: DataViewField) => {
+ const conflictsInfo = getMappingConflictsInfo(field);
+ if (!conflictsInfo) {
+ return acc;
+ }
+ acc[field.name] = conflictsInfo;
+ return acc;
+ },
+ {}
+ );
+ return mappingConflictsTooltipInfo;
+};
+
const getComboBoxProps = (fields: ComboBoxFields): GetFieldComboBoxPropsReturn => {
const { availableFields, selectedFields } = fields;
@@ -81,9 +105,11 @@ const getComboBoxProps = (fields: ComboBoxFields): GetFieldComboBoxPropsReturn =
selectedOptions: selectedFields,
});
const disabledLabelTooltipTexts = getDisabledLabelTooltipTexts(fields);
+ const mappingConflictsTooltipInfo = getMappingConflictsTooltipInfo(fields);
return {
...genericProps,
disabledLabelTooltipTexts,
+ mappingConflictsTooltipInfo,
};
};
@@ -93,11 +119,13 @@ export const useField = ({
isRequired,
selectedField,
fieldInputWidth,
+ showMappingConflicts,
onChange,
}: FieldBaseProps) => {
const [touched, setIsTouched] = useState(false);
const [customOption, setCustomOption] = useState(null);
+ const sPaddingSize = useEuiPaddingSize('s');
const { availableFields, selectedFields } = useMemo(() => {
const indexPatternsToUse =
@@ -107,7 +135,13 @@ export const useField = ({
return getComboBoxFields(indexPatternsToUse, selectedField, fieldTypeFilter);
}, [indexPattern, fieldTypeFilter, selectedField, customOption]);
- const { comboOptions, labels, selectedComboOptions, disabledLabelTooltipTexts } = useMemo(
+ const {
+ comboOptions,
+ labels,
+ selectedComboOptions,
+ disabledLabelTooltipTexts,
+ mappingConflictsTooltipInfo,
+ } = useMemo(
() => getComboBoxProps({ availableFields, selectedFields }),
[availableFields, selectedFields]
);
@@ -168,6 +202,46 @@ export const useField = ({
);
}
+
+ const conflictsInfo = mappingConflictsTooltipInfo[label];
+ if (showMappingConflicts && conflictsInfo) {
+ const tooltipContent = (
+ <>
+ {i18n.FIELD_CONFLICT_INDICES_WARNING_DESCRIPTION}
+ {conflictsInfo.map((info) => {
+ const groupDetails = info.groupedIndices.map(
+ ({ name, count }) =>
+ `${count > 1 ? i18n.CONFLICT_MULTIPLE_INDEX_DESCRIPTION(name, count) : name}`
+ );
+ return (
+ <>
+
+ {`${
+ info.totalIndexCount > 1
+ ? i18n.CONFLICT_MULTIPLE_INDEX_DESCRIPTION(info.type, info.totalIndexCount)
+ : info.type
+ }: ${groupDetails.join(', ')}`}
+ >
+ );
+ })}
+ >
+ );
+ return (
+
+ <>
+ {label}
+
+ >
+
+ );
+ }
+
return label;
};
return {
diff --git a/packages/kbn-securitysolution-autocomplete/src/translations/index.ts b/packages/kbn-securitysolution-autocomplete/src/translations/index.ts
index df54ada4fb205..5267e213fa5c3 100644
--- a/packages/kbn-securitysolution-autocomplete/src/translations/index.ts
+++ b/packages/kbn-securitysolution-autocomplete/src/translations/index.ts
@@ -43,6 +43,26 @@ export const SEE_DOCUMENTATION = i18n.translate('autocomplete.seeDocumentation',
defaultMessage: 'See Documentation',
});
+export const FIELD_CONFLICT_INDICES_WARNING_TITLE = i18n.translate(
+ 'autocomplete.conflictIndicesWarning.title',
+ {
+ defaultMessage: 'Mapping Conflict',
+ }
+);
+
+export const FIELD_CONFLICT_INDICES_WARNING_DESCRIPTION = i18n.translate(
+ 'autocomplete.conflictIndicesWarning.description',
+ {
+ defaultMessage: 'This field is defined as several types across different indices.',
+ }
+);
+
+export const CONFLICT_MULTIPLE_INDEX_DESCRIPTION = (name: string, count: number): string =>
+ i18n.translate('autocomplete.conflictIndicesWarning.index.description', {
+ defaultMessage: '{name} ({count} indices)',
+ values: { count, name },
+ });
+
// eslint-disable-next-line import/no-default-export
export default {
LOADING,
diff --git a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts
index d2f84b4ee3708..ab1f35c54b5db 100644
--- a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts
+++ b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts
@@ -16,6 +16,7 @@ describe('importQuerySchema', () => {
as_new_list: false,
overwrite: true,
overwrite_exceptions: true,
+ overwrite_action_connectors: true,
};
const decoded = importQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -30,6 +31,7 @@ describe('importQuerySchema', () => {
as_new_list: false,
overwrite: 'wrong',
overwrite_exceptions: true,
+ overwrite_action_connectors: true,
};
const decoded = importQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -48,6 +50,7 @@ describe('importQuerySchema', () => {
as_new_list: false,
overwrite: true,
overwrite_exceptions: 'wrong',
+ overwrite_action_connectors: true,
};
const decoded = importQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -58,6 +61,24 @@ describe('importQuerySchema', () => {
]);
expect(message.schema).toEqual({});
});
+ test('it should NOT validate a non boolean value for "overwrite_action_connectors"', () => {
+ const payload: Omit & {
+ overwrite_action_connectors: string;
+ } = {
+ as_new_list: false,
+ overwrite: true,
+ overwrite_exceptions: true,
+ overwrite_action_connectors: 'wrong',
+ };
+ const decoded = importQuerySchema.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = foldLeftRight(checked);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "wrong" supplied to "overwrite_action_connectors"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
test('it should NOT allow an extra key to be sent in', () => {
const payload: ImportQuerySchema & {
diff --git a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts
index ca0e35c4aa176..5b1ee38bb0855 100644
--- a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts
+++ b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts
@@ -14,6 +14,7 @@ export const importQuerySchema = t.exact(
t.partial({
overwrite: DefaultStringBooleanFalse,
overwrite_exceptions: DefaultStringBooleanFalse,
+ overwrite_action_connectors: DefaultStringBooleanFalse,
as_new_list: DefaultStringBooleanFalse,
})
);
@@ -21,9 +22,10 @@ export const importQuerySchema = t.exact(
export type ImportQuerySchema = t.TypeOf;
export type ImportQuerySchemaDecoded = Omit<
ImportQuerySchema,
- 'overwrite' | 'overwrite_exceptions' | 'as_new_list'
+ 'overwrite' | 'overwrite_exceptions' | 'as_new_list' | 'overwrite_action_connectors'
> & {
overwrite: boolean;
overwrite_exceptions: boolean;
+ overwrite_action_connectors: boolean;
as_new_list: boolean;
};
diff --git a/packages/kbn-securitysolution-list-utils/src/helpers/index.test.ts b/packages/kbn-securitysolution-list-utils/src/helpers/index.test.ts
new file mode 100644
index 0000000000000..31a5ba496647f
--- /dev/null
+++ b/packages/kbn-securitysolution-list-utils/src/helpers/index.test.ts
@@ -0,0 +1,146 @@
+/*
+ * 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 { getMappingConflictsInfo } from '.';
+
+describe('Helpers', () => {
+ describe('getMappingConflictsInfo', () => {
+ test('it return null if there are not conflicts', () => {
+ const field = {
+ name: 'field1',
+ type: 'string',
+ };
+ const conflictsInfo = getMappingConflictsInfo(field);
+
+ expect(conflictsInfo).toBeNull();
+ });
+ test('it groups ".ds-" data stream indices', () => {
+ const field = {
+ name: 'field1',
+ type: 'conflict',
+ conflictDescriptions: {
+ text: [
+ '.ds-logs-default-2023.01.18-000001',
+ '.ds-logs-default-2023.01.18-000002',
+ '.ds-logs-tortilla.process-default-2022.11.20-000011',
+ '.ds-logs-tortilla.process-default-2022.11.20-000012',
+ '.ds-logs-tortilla.process-default-2022.11.20-000016',
+ ],
+ long: [
+ '.ds-logs-default-2023.01.18-000004',
+ '.ds-logs-default-2023.01.18-000005',
+ 'partial-.ds-logs-gcp.audit-2021.12.22-000240',
+ 'partial-.ds-logs-gcp.audit-2021.12.22-000242',
+ ],
+ },
+ };
+ const conflictsInfo = getMappingConflictsInfo(field);
+
+ expect(conflictsInfo).toEqual([
+ {
+ type: 'text',
+ totalIndexCount: 5,
+ groupedIndices: [
+ { name: 'logs-tortilla.process-default', count: 3 },
+ { name: 'logs-default', count: 2 },
+ ],
+ },
+ {
+ type: 'long',
+ totalIndexCount: 4,
+ groupedIndices: [
+ { name: 'logs-default', count: 2 },
+ { name: 'logs-gcp.audit', count: 2 },
+ ],
+ },
+ ]);
+ });
+ test('it groups old ".siem-" indices', () => {
+ const field = {
+ name: 'field1',
+ type: 'conflict',
+ conflictDescriptions: {
+ text: [
+ '.siem-signals-default-000001',
+ '.siem-signals-default-000002',
+ '.siem-signals-default-000011',
+ '.siem-signals-default-000012',
+ ],
+ unmapped: [
+ '.siem-signals-default-000004',
+ '.siem-signals-default-000005',
+ '.siem-signals-default-000240',
+ ],
+ },
+ };
+ const conflictsInfo = getMappingConflictsInfo(field);
+
+ expect(conflictsInfo).toEqual([
+ {
+ type: 'text',
+ totalIndexCount: 4,
+ groupedIndices: [{ name: '.siem-signals-default', count: 4 }],
+ },
+ {
+ type: 'unmapped',
+ totalIndexCount: 3,
+ groupedIndices: [{ name: '.siem-signals-default', count: 3 }],
+ },
+ ]);
+ });
+ test('it groups mixed indices', () => {
+ const field = {
+ name: 'field1',
+ type: 'conflict',
+ conflictDescriptions: {
+ boolean: [
+ '.ds-logs-default-2023.01.18-000001',
+ '.ds-logs-tortilla.process-default-2022.11.20-000011',
+ '.ds-logs-tortilla.process-default-2022.11.20-000012',
+ '.ds-logs-tortilla.process-default-2022.11.20-000016',
+ '.siem-signals-default-000001',
+ '.siem-signals-default-000002',
+ '.siem-signals-default-000012',
+ 'my-own-index-1',
+ 'my-own-index-2',
+ ],
+ unmapped: [
+ '.siem-signals-default-000004',
+ 'partial-.ds-logs-gcp.audit-2021.12.22-000240',
+ 'partial-.ds-logs-gcp.audit-2021.12.22-000242',
+ 'my-own-index-3',
+ ],
+ },
+ };
+ const conflictsInfo = getMappingConflictsInfo(field);
+
+ expect(conflictsInfo).toEqual([
+ {
+ type: 'boolean',
+ totalIndexCount: 9,
+ groupedIndices: [
+ { name: 'logs-tortilla.process-default', count: 3 },
+ { name: '.siem-signals-default', count: 3 },
+ { name: 'logs-default', count: 1 },
+ { name: 'my-own-index-1', count: 1 },
+ { name: 'my-own-index-2', count: 1 },
+ ],
+ },
+ {
+ type: 'unmapped',
+ totalIndexCount: 4,
+ groupedIndices: [
+ { name: 'logs-gcp.audit', count: 2 },
+ { name: '.siem-signals-default', count: 1 },
+ { name: 'my-own-index-3', count: 1 },
+ ],
+ },
+ ]);
+ });
+ });
+});
diff --git a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
index 638dead59e366..38a41393b867c 100644
--- a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
+++ b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
@@ -53,6 +53,7 @@ import {
import {
BuilderEntry,
CreateExceptionListItemBuilderSchema,
+ DataViewField,
EmptyEntry,
EmptyNestedEntry,
ExceptionsBuilderExceptionItem,
@@ -912,3 +913,87 @@ export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({
export const containsValueListEntry = (items: ExceptionsBuilderExceptionItem[]): boolean =>
items.some((item) => item.entries.some(({ type }) => type === OperatorTypeEnum.LIST));
+
+const getIndexGroupName = (indexName: string): string => {
+ // Check whether it is a Data Stream index
+ const dataStreamExp = /.ds-(.*?)-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[0-9]{6}/;
+ let result = indexName.match(dataStreamExp);
+ if (result && result.length === 2) {
+ return result[1];
+ }
+
+ // Check whether it is an old '.siem' index group
+ const siemSignalsExp = /.siem-(.*?)-[0-9]{6}/;
+ result = indexName.match(siemSignalsExp);
+ if (result && result.length === 2) {
+ return `.siem-${result[1]}`;
+ }
+
+ // Otherwise return index name
+ return indexName;
+};
+
+export interface FieldConflictsInfo {
+ /**
+ * Kibana field type
+ */
+ type: string;
+ /**
+ * Total count of the indices of this type
+ */
+ totalIndexCount: number;
+ /**
+ * Grouped indices info
+ */
+ groupedIndices: Array<{
+ /**
+ * Index group name (like '.ds-...' or '.siem-signals-...')
+ */
+ name: string;
+ /**
+ * Count of indices in the group
+ */
+ count: number;
+ }>;
+}
+
+export const getMappingConflictsInfo = (field: DataViewField): FieldConflictsInfo[] | null => {
+ if (!field.conflictDescriptions) {
+ return null;
+ }
+ const conflicts: FieldConflictsInfo[] = [];
+ for (const [key, value] of Object.entries(field.conflictDescriptions)) {
+ const groupedIndices: Array<{
+ name: string;
+ count: number;
+ }> = [];
+
+ // Group indices and calculate count of indices in each group
+ const groupedInfo: { [key: string]: number } = {};
+ value.forEach((index) => {
+ const groupName = getIndexGroupName(index);
+ if (!groupedInfo[groupName]) {
+ groupedInfo[groupName] = 0;
+ }
+ groupedInfo[groupName]++;
+ });
+ for (const [name, count] of Object.entries(groupedInfo)) {
+ groupedIndices.push({
+ name,
+ count,
+ });
+ }
+
+ // Sort groups by the indices count
+ groupedIndices.sort((group1, group2) => {
+ return group2.count - group1.count;
+ });
+
+ conflicts.push({
+ type: key,
+ totalIndexCount: value.length,
+ groupedIndices,
+ });
+ }
+ return conflicts;
+};
diff --git a/packages/kbn-securitysolution-list-utils/src/types/index.ts b/packages/kbn-securitysolution-list-utils/src/types/index.ts
index 13b62fac657a2..ccd0ab68799bb 100644
--- a/packages/kbn-securitysolution-list-utils/src/types/index.ts
+++ b/packages/kbn-securitysolution-list-utils/src/types/index.ts
@@ -117,3 +117,7 @@ export const exceptionListAgnosticSavedObjectType = EXCEPTION_LIST_NAMESPACE_AGN
export type SavedObjectType =
| typeof EXCEPTION_LIST_NAMESPACE
| typeof EXCEPTION_LIST_NAMESPACE_AGNOSTIC;
+
+export interface DataViewField extends DataViewFieldBase {
+ conflictDescriptions?: Record;
+}
diff --git a/packages/shared-ux/prompt/not_found/README.mdx b/packages/shared-ux/prompt/not_found/README.mdx
index dc784f86b3854..3db21640d5760 100644
--- a/packages/shared-ux/prompt/not_found/README.mdx
+++ b/packages/shared-ux/prompt/not_found/README.mdx
@@ -10,3 +10,27 @@ date: 2022-02-09
Sometimes the user tries to go to a page that doesn't exist, because the URL is broken or because they try to load a resource that does not exist. For those cases we want to show a standard 404 error.
The default call to action is a "Go back" button that simulates the browser's "Back" behaviour. Consumers can specify their own CTA's with the `actions` prop.
+
+The NotFoundPrompt component also has the prop `actions` that takes in an array of components.
+For example:
+
+```jsx
+
+
+
+ Go home
+ ,
+
+ Go to discover
+
+ ]
+ }
+ title="Customizable Title"
+ body="Customizable Body"
+ />
+
+
+```
\ No newline at end of file
diff --git a/packages/shared-ux/prompt/not_found/src/not_found_prompt.stories.tsx b/packages/shared-ux/prompt/not_found/src/not_found_prompt.stories.tsx
index 4cc103140929e..f5dccc68d9c97 100644
--- a/packages/shared-ux/prompt/not_found/src/not_found_prompt.stories.tsx
+++ b/packages/shared-ux/prompt/not_found/src/not_found_prompt.stories.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { EuiButton, EuiButtonEmpty, EuiPageTemplate } from '@elastic/eui';
+import { EuiButton, EuiPageTemplate } from '@elastic/eui';
import React from 'react';
import { Meta, Story } from '@storybook/react';
import mdx from '../README.mdx';
@@ -51,14 +51,18 @@ export const CustomActions: Story = (args) => {
- Go home
- ,
-
- Go to discover
- ,
- ]}
+ actions={
+ <>
+
+ Go home
+
+
+ Go to discover
+
+ >
+ }
+ title="Customizable Title"
+ body="Customizable Body"
/>
diff --git a/packages/shared-ux/prompt/not_found/src/not_found_prompt.test.tsx b/packages/shared-ux/prompt/not_found/src/not_found_prompt.test.tsx
index 48f0d82520f30..da836d48f9266 100644
--- a/packages/shared-ux/prompt/not_found/src/not_found_prompt.test.tsx
+++ b/packages/shared-ux/prompt/not_found/src/not_found_prompt.test.tsx
@@ -36,4 +36,11 @@ describe(' ', () => {
const component = render( );
expect(component.text()).toContain('I am a button');
});
+
+ it('Renders custom actions with an array with multiple buttons', () => {
+ const actions = [I am a button , I am a second button ];
+ const component = render( );
+ expect(component.text()).toContain('I am a button');
+ expect(component.text()).toContain('I am a second button');
+ });
});
diff --git a/renovate.json b/renovate.json
index 932f0a6558864..ce12b9dd5a248 100644
--- a/renovate.json
+++ b/renovate.json
@@ -153,6 +153,17 @@
"labels": ["Team:Operations", "release_note:skip", "backport:all-open"],
"enabled": true
},
+ {
+ "groupName": "minify",
+ "packageNames": [
+ "gulp-terser",
+ "terser"
+ ],
+ "reviewers": ["team:kibana-operations"],
+ "matchBaseBranches": ["main"],
+ "labels": ["Team:Operations", "release_note:skip"],
+ "enabled": true
+ },
{
"groupName": "@testing-library",
"packageNames": [
diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts
index 5ae02ab100926..0357fb698318a 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts
@@ -74,7 +74,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"cases-configure": "1afc414f5563a36e4612fa269193d3ed7277c7bd",
"cases-connector-mappings": "4b16d440af966e5d6e0fa33368bfa15d987a4b69",
"cases-telemetry": "16e261e7378a72acd0806f18df92525dd1da4f37",
- "cases-user-actions": "f1b0dcfeb58a65e68b35c5e99ddee70e746a06c7",
+ "cases-user-actions": "9570938b5832f0e91ca489d8e1edf7dcaef59e55",
"config": "95d8ecc452186201a691e72ec154ba65dfc5cb98",
"config-global": "b8f559884931609a349e129c717af73d23e7bc76",
"connector_token": "fa5301aa5a2914795d3b1b82d0a49939444009da",
diff --git a/src/plugins/charts/public/static/components/endzones.tsx b/src/plugins/charts/public/static/components/endzones.tsx
index e89ec60fd5a8b..fed388c5123f6 100644
--- a/src/plugins/charts/public/static/components/endzones.tsx
+++ b/src/plugins/charts/public/static/components/endzones.tsx
@@ -14,6 +14,7 @@ import {
RectAnnotation,
RectAnnotationDatum,
RectAnnotationStyle,
+ AnnotationTooltipFormatter,
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, useEuiTheme } from '@elastic/eui';
@@ -142,7 +143,7 @@ const partialDataText = i18n.translate('charts.partialData.bucketTooltipText', {
'The selected time range does not include this entire bucket. It might contain partial data.',
});
-const Prompt = () => {
+const Prompt: AnnotationTooltipFormatter = () => {
const { euiTheme } = useEuiTheme();
const headerPartialCss = css`
font-weight: ${euiTheme.font.weight.regular};
diff --git a/src/plugins/console/common/constants/welcome_panel.ts b/src/plugins/console/common/constants/welcome_panel.ts
new file mode 100644
index 0000000000000..dd5c17d8c9f8e
--- /dev/null
+++ b/src/plugins/console/common/constants/welcome_panel.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 const kibanaApiExample = `
+# retrieve sets of saved objects
+POST kbn:api/saved_objects/_export
+`;
+
+export const multipleRequestsExample = `
+# POST test_index/_doc/test_doc 200 OK
+{
+ "_index": "test_index",
+ "_id": "test_doc",
+ "_version": 3,
+ "result": "updated",
+ "_shards": {
+ "total": 2,
+ "successful": 1,
+ "failed": 0
+ },
+ "_seq_no": 2,
+ "_primary_term": 1
+}
+# POST notAnEndpoint 405 Method Not Allowed
+{
+ "error": "Incorrect HTTP method for uri [/notAnEndpoint?pretty=true] and method [POST], allowed: [HEAD, GET, PUT, DELETE]",
+ "status": 405
+}
+`;
+
+export const commentsExample = `
+# This request searches all of your indices.
+GET _search
+{
+ // The query parameter indicates query context.
+ "query": {
+ // Matches all documents.
+ /*"match_all": {
+ "boost": 1.2
+ }*/
+ "match_none": {} // Matches no document.
+ }
+}
+`;
+
+export const variablesExample = `
+GET $\{pathVariable}
+{
+ "query": {
+ "match": {
+ "$\{bodyNameVariable}": "$\{bodyValueVariable}"
+ }
+ }
+}
+`;
diff --git a/src/plugins/console/public/application/components/editor_example.tsx b/src/plugins/console/public/application/components/editor_example.tsx
index 21e3ab0c7d274..4c456a2de44c1 100644
--- a/src/plugins/console/public/application/components/editor_example.tsx
+++ b/src/plugins/console/public/application/components/editor_example.tsx
@@ -6,30 +6,40 @@
* Side Public License, v 1.
*/
-import { EuiScreenReaderOnly } from '@elastic/eui';
+import { EuiScreenReaderOnly, withEuiTheme } from '@elastic/eui';
+import type { WithEuiThemeProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useRef } from 'react';
import { createReadOnlyAceEditor, CustomAceEditor } from '../models/sense_editor';
// @ts-ignore
-import { Mode } from '../models/legacy_core_editor/mode/input';
+import { Mode as InputMode } from '../models/legacy_core_editor/mode/input';
+import { Mode as OutputMode } from '../models/legacy_core_editor/mode/output';
interface EditorExampleProps {
panel: string;
+ example?: string;
+ theme: WithEuiThemeProps['theme'];
+ linesOfExampleCode?: number;
+ mode?: string;
}
const exampleText = `
-# index a doc
-PUT index/_doc/1
+GET _search
{
- "body": "here"
+ "query": {
+ "match_all": {}
+ }
}
-
-# and get it ...
-GET index/_doc/1
`;
-export function EditorExample(props: EditorExampleProps) {
- const inputId = `help-example-${props.panel}-input`;
+const EditorExample = ({
+ panel,
+ example,
+ theme,
+ linesOfExampleCode = 6,
+ mode = 'input',
+}: EditorExampleProps) => {
+ const inputId = `help-example-${panel}-input`;
const wrapperDivRef = useRef(null);
const editorRef = useRef();
@@ -38,8 +48,8 @@ export function EditorExample(props: EditorExampleProps) {
editorRef.current = createReadOnlyAceEditor(wrapperDivRef.current);
const editor = editorRef.current;
- editor.update(exampleText.trim());
- editor.session.setMode(new Mode());
+ const editorMode = mode === 'input' ? new InputMode() : new OutputMode();
+ editor.update((example || exampleText).trim(), editorMode);
editor.session.setUseWorker(false);
editor.setHighlightActiveLine(false);
@@ -55,7 +65,12 @@ export function EditorExample(props: EditorExampleProps) {
editorRef.current.destroy();
}
};
- }, [inputId]);
+ }, [example, inputId, mode]);
+
+ const wrapperDivStyle = {
+ height: `${parseInt(theme.euiTheme.size.base, 10) * linesOfExampleCode}px`,
+ margin: `${theme.euiTheme.size.base} 0`,
+ };
return (
<>
@@ -66,7 +81,10 @@ export function EditorExample(props: EditorExampleProps) {
})}
-
+
>
);
-}
+};
+
+// eslint-disable-next-line import/no-default-export
+export default withEuiTheme(EditorExample);
diff --git a/src/plugins/console/public/application/components/help_panel.tsx b/src/plugins/console/public/application/components/help_panel.tsx
index ed57d3178df4b..a939899af1879 100644
--- a/src/plugins/console/public/application/components/help_panel.tsx
+++ b/src/plugins/console/public/application/components/help_panel.tsx
@@ -17,7 +17,7 @@ import {
EuiSpacer,
EuiLink,
} from '@elastic/eui';
-import { EditorExample } from './editor_example';
+import EditorExample from './editor_example';
import { useServicesContext } from '../contexts';
interface Props {
diff --git a/src/plugins/console/public/application/components/welcome_panel.tsx b/src/plugins/console/public/application/components/welcome_panel.tsx
index 3a7898df084a7..9b2741e4c30e0 100644
--- a/src/plugins/console/public/application/components/welcome_panel.tsx
+++ b/src/plugins/console/public/application/components/welcome_panel.tsx
@@ -18,8 +18,10 @@ import {
EuiButton,
EuiText,
EuiFlyoutFooter,
+ EuiCode,
} from '@elastic/eui';
-import { EditorExample } from './editor_example';
+import EditorExample from './editor_example';
+import * as examples from '../../../common/constants/welcome_panel';
interface Props {
onDismiss: () => void;
@@ -27,85 +29,127 @@ interface Props {
export function WelcomePanel(props: Props) {
return (
-
+
-
+
-
+
+
kbn:,
+ }}
/>
+
+
+
+
-
+
+
+
+
+
#,
+ doubleSlash: // ,
+ slashAsterisk: /* ,
+ asteriskSlash: */ ,
+ }}
/>
+
-
-
-
-
-
-
-
-
-
-
+
+ ${variableName},
+ }}
+ />
+
+
+
Variables,
+ }}
/>
-
+
+
diff --git a/src/plugins/console/public/application/containers/editor/editor.tsx b/src/plugins/console/public/application/containers/editor/editor.tsx
index 93c11aa53c602..788039a2dc606 100644
--- a/src/plugins/console/public/application/containers/editor/editor.tsx
+++ b/src/plugins/console/public/application/containers/editor/editor.tsx
@@ -6,14 +6,14 @@
* Side Public License, v 1.
*/
-import React, { useCallback, memo } from 'react';
+import React, { useCallback, memo, useEffect, useState } from 'react';
import { debounce } from 'lodash';
import { EuiProgress } from '@elastic/eui';
import { EditorContentSpinner } from '../../components';
import { Panel, PanelsContainer } from '..';
import { Editor as EditorUI, EditorOutput } from './legacy/console_editor';
-import { StorageKeys } from '../../../services';
+import { getAutocompleteInfo, StorageKeys } from '../../../services';
import { useEditorReadContext, useServicesContext, useRequestReadContext } from '../../contexts';
import type { SenseEditor } from '../../models';
@@ -33,6 +33,15 @@ export const Editor = memo(({ loading, setEditorInstance }: Props) => {
const { currentTextObject } = useEditorReadContext();
const { requestInFlight } = useRequestReadContext();
+ const [fetchingMappings, setFetchingMappings] = useState(false);
+
+ useEffect(() => {
+ const subscription = getAutocompleteInfo().mapping.isLoading$.subscribe(setFetchingMappings);
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, []);
+
const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [
INITIAL_PANEL_WIDTH,
INITIAL_PANEL_WIDTH,
@@ -50,7 +59,7 @@ export const Editor = memo(({ loading, setEditorInstance }: Props) => {
return (
<>
- {requestInFlight ? (
+ {requestInFlight || fetchingMappings ? (
diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx
index 1cf9a54210973..92d875b9db2a9 100644
--- a/src/plugins/console/public/application/index.tsx
+++ b/src/plugins/console/public/application/index.tsx
@@ -69,6 +69,8 @@ export function renderApp({
const api = createApi({ http });
const esHostService = createEsHostService({ api });
+ autocompleteInfo.mapping.setup(http, settings);
+
render(
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts
index 5def8a696df2f..5c041a95e216f 100644
--- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts
+++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import ace from 'brace';
+import ace, { type Annotation } from 'brace';
import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace';
import $ from 'jquery';
import {
@@ -402,8 +402,7 @@ export class LegacyCoreEditor implements CoreEditor {
getCompletions: (
// eslint-disable-next-line @typescript-eslint/naming-convention
DO_NOT_USE_1: IAceEditor,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- DO_NOT_USE_2: IAceEditSession,
+ aceEditSession: IAceEditSession,
pos: { row: number; column: number },
prefix: string,
callback: (...args: unknown[]) => void
@@ -412,7 +411,30 @@ export class LegacyCoreEditor implements CoreEditor {
lineNumber: pos.row + 1,
column: pos.column + 1,
};
- autocompleter(position, prefix, callback);
+
+ const getAnnotationControls = () => {
+ let customAnnotation: Annotation;
+ return {
+ setAnnotation(text: string) {
+ const annotations = aceEditSession.getAnnotations();
+ customAnnotation = {
+ text,
+ row: pos.row,
+ column: pos.column,
+ type: 'warning',
+ };
+
+ aceEditSession.setAnnotations([...annotations, customAnnotation]);
+ },
+ removeAnnotation() {
+ aceEditSession.setAnnotations(
+ aceEditSession.getAnnotations().filter((a: Annotation) => a !== customAnnotation)
+ );
+ },
+ };
+ };
+
+ autocompleter(position, prefix, callback, getAnnotationControls());
},
},
]);
diff --git a/src/plugins/console/public/application/models/sense_editor/integration.test.js b/src/plugins/console/public/application/models/sense_editor/integration.test.js
index 434deb5456caf..08912871052ba 100644
--- a/src/plugins/console/public/application/models/sense_editor/integration.test.js
+++ b/src/plugins/console/public/application/models/sense_editor/integration.test.js
@@ -13,6 +13,9 @@ import $ from 'jquery';
import * as kb from '../../../lib/kb/kb';
import { AutocompleteInfo, setAutocompleteInfo } from '../../../services';
+import { httpServiceMock } from '@kbn/core-http-browser-mocks';
+import { StorageMock } from '../../../services/storage.mock';
+import { SettingsMock } from '../../../services/settings.mock';
describe('Integration', () => {
let senseEditor;
@@ -27,6 +30,15 @@ describe('Integration', () => {
$(senseEditor.getCoreEditor().getContainer()).show();
senseEditor.autocomplete._test.removeChangeListener();
autocompleteInfo = new AutocompleteInfo();
+
+ const httpMock = httpServiceMock.createSetupContract();
+ const storage = new StorageMock({}, 'test');
+ const settingsMock = new SettingsMock(storage);
+
+ settingsMock.getAutocomplete.mockReturnValue({ fields: true });
+
+ autocompleteInfo.mapping.setup(httpMock, settingsMock);
+
setAutocompleteInfo(autocompleteInfo);
});
afterEach(() => {
@@ -164,7 +176,8 @@ describe('Integration', () => {
ac('textBoxPosition', posCompare);
ac('rangeToReplace', rangeCompare);
done();
- }
+ },
+ { setAnnotation: () => {}, removeAnnotation: () => {} }
);
});
});
diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts
index 4e3770779f580..b24767b361d7e 100644
--- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts
+++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts
@@ -593,7 +593,10 @@ export default function ({
return null;
}
- if (!context.autoCompleteSet) {
+ const isMappingsFetchingInProgress =
+ context.autoCompleteType === 'body' && !!context.asyncResultsState?.isLoading;
+
+ if (!context.autoCompleteSet && !isMappingsFetchingInProgress) {
return null; // nothing to do..
}
@@ -1123,80 +1126,112 @@ export default function ({
}
}
+ /**
+ * Extracts terms from the autocomplete set.
+ * @param context
+ */
+ function getTerms(context: AutoCompleteContext, autoCompleteSet: ResultTerm[]) {
+ const terms = _.map(
+ autoCompleteSet.filter((term) => Boolean(term) && term.name != null),
+ function (term) {
+ if (typeof term !== 'object') {
+ term = {
+ name: term,
+ };
+ } else {
+ term = _.clone(term);
+ }
+ const defaults: {
+ value?: string;
+ meta: string;
+ score: number;
+ context: AutoCompleteContext;
+ completer?: { insertMatch: (v: unknown) => void };
+ } = {
+ value: term.name,
+ meta: 'API',
+ score: 0,
+ context,
+ };
+ // we only need our custom insertMatch behavior for the body
+ if (context.autoCompleteType === 'body') {
+ defaults.completer = {
+ insertMatch() {
+ return applyTerm(term);
+ },
+ };
+ }
+ return _.defaults(term, defaults);
+ }
+ );
+
+ terms.sort(function (
+ t1: { score: number; name?: string },
+ t2: { score: number; name?: string }
+ ) {
+ /* score sorts from high to low */
+ if (t1.score > t2.score) {
+ return -1;
+ }
+ if (t1.score < t2.score) {
+ return 1;
+ }
+ /* names sort from low to high */
+ if (t1.name! < t2.name!) {
+ return -1;
+ }
+ if (t1.name === t2.name) {
+ return 0;
+ }
+ return 1;
+ });
+
+ return terms;
+ }
+
+ function getSuggestions(terms: ResultTerm[]) {
+ return _.map(terms, function (t, i) {
+ t.insertValue = t.insertValue || t.value;
+ t.value = '' + t.value; // normalize to strings
+ t.score = -i;
+ return t;
+ });
+ }
+
function getCompletions(
position: Position,
prefix: string,
- callback: (e: Error | null, result: ResultTerm[] | null) => void
+ callback: (e: Error | null, result: ResultTerm[] | null) => void,
+ annotationControls: {
+ setAnnotation: (text: string) => void;
+ removeAnnotation: () => void;
+ }
) {
try {
const context = getAutoCompleteContext(editor, position);
+
if (!context) {
callback(null, []);
} else {
- const terms = _.map(
- context.autoCompleteSet!.filter((term) => Boolean(term) && term.name != null),
- function (term) {
- if (typeof term !== 'object') {
- term = {
- name: term,
- };
- } else {
- term = _.clone(term);
- }
- const defaults: {
- value?: string;
- meta: string;
- score: number;
- context: AutoCompleteContext;
- completer?: { insertMatch: (v: unknown) => void };
- } = {
- value: term.name,
- meta: 'API',
- score: 0,
- context,
- };
- // we only need our custom insertMatch behavior for the body
- if (context.autoCompleteType === 'body') {
- defaults.completer = {
- insertMatch() {
- return applyTerm(term);
- },
- };
- }
- return _.defaults(term, defaults);
- }
- );
-
- terms.sort(function (
- t1: { score: number; name?: string },
- t2: { score: number; name?: string }
- ) {
- /* score sorts from high to low */
- if (t1.score > t2.score) {
- return -1;
- }
- if (t1.score < t2.score) {
- return 1;
- }
- /* names sort from low to high */
- if (t1.name! < t2.name!) {
- return -1;
- }
- if (t1.name === t2.name) {
- return 0;
- }
- return 1;
- });
-
- callback(
- null,
- _.map(terms, function (t, i) {
- t.insertValue = t.insertValue || t.value;
- t.value = '' + t.value; // normalize to strings
- t.score = -i;
- return t;
- })
- );
+ if (!context.asyncResultsState?.isLoading) {
+ const terms = getTerms(context, context.autoCompleteSet!);
+ const suggestions = getSuggestions(terms);
+ callback(null, suggestions);
+ }
+
+ if (context.asyncResultsState) {
+ annotationControls.setAnnotation(
+ i18n.translate('console.autocomplete.fieldsFetchingAnnotation', {
+ defaultMessage: 'Fields fetching is in progress',
+ })
+ );
+
+ context.asyncResultsState.results.then((r) => {
+ const asyncSuggestions = getSuggestions(getTerms(context, r));
+ callback(null, asyncSuggestions);
+ annotationControls.removeAnnotation();
+ });
+ }
}
} catch (e) {
// eslint-disable-next-line no-console
@@ -1216,8 +1251,12 @@ export default function ({
_editSession: unknown,
pos: Position,
prefix: string,
- callback: (e: Error | null, result: ResultTerm[] | null) => void
- ) => getCompletions(pos, prefix, callback),
+ callback: (e: Error | null, result: ResultTerm[] | null) => void,
+ annotationControls: {
+ setAnnotation: (text: string) => void;
+ removeAnnotation: () => void;
+ }
+ ) => getCompletions(pos, prefix, callback, annotationControls),
addReplacementInfoToContext,
addChangeListener: () => editor.on('changeSelection', editorChangeListener),
removeChangeListener: () => editor.off('changeSelection', editorChangeListener),
diff --git a/src/plugins/console/public/lib/autocomplete/types.ts b/src/plugins/console/public/lib/autocomplete/types.ts
index 15d32e6426a6c..a151c13f46c20 100644
--- a/src/plugins/console/public/lib/autocomplete/types.ts
+++ b/src/plugins/console/public/lib/autocomplete/types.ts
@@ -13,6 +13,7 @@ export interface ResultTerm {
insertValue?: string;
name?: string;
value?: string;
+ score?: number;
}
export interface DataAutoCompleteRulesOneOf {
@@ -25,6 +26,14 @@ export interface DataAutoCompleteRulesOneOf {
export interface AutoCompleteContext {
autoCompleteSet?: null | ResultTerm[];
+ /**
+ * Stores a state for async results, e.g. fields suggestions based on the mappings definition.
+ */
+ asyncResultsState?: {
+ isLoading: boolean;
+ lastFetched: number | null;
+ results: Promise;
+ };
endpoint?: null | {
paramsAutocomplete: {
getTopLevelComponents: (method?: string | null) => unknown;
diff --git a/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js b/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js
index 5349538799d9b..f856ef5750a17 100644
--- a/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js
+++ b/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js
@@ -9,6 +9,9 @@
import '../../application/models/sense_editor/sense_editor.test.mocks';
import { setAutocompleteInfo, AutocompleteInfo } from '../../services';
import { expandAliases } from './expand_aliases';
+import { httpServiceMock } from '@kbn/core-http-browser-mocks';
+import { SettingsMock } from '../../services/settings.mock';
+import { StorageMock } from '../../services/storage.mock';
function fc(f1, f2) {
if (f1.name < f2.name) {
@@ -32,10 +35,20 @@ describe('Autocomplete entities', () => {
let componentTemplate;
let dataStream;
let autocompleteInfo;
+ let settingsMock;
+ let httpMock;
+
beforeEach(() => {
autocompleteInfo = new AutocompleteInfo();
setAutocompleteInfo(autocompleteInfo);
mapping = autocompleteInfo.mapping;
+
+ httpMock = httpServiceMock.createSetupContract();
+ const storage = new StorageMock({}, 'test');
+ settingsMock = new SettingsMock(storage);
+
+ mapping.setup(httpMock, settingsMock);
+
alias = autocompleteInfo.alias;
legacyTemplate = autocompleteInfo.legacyTemplate;
indexTemplate = autocompleteInfo.indexTemplate;
@@ -48,61 +61,98 @@ describe('Autocomplete entities', () => {
});
describe('Mappings', function () {
- test('Multi fields 1.0 style', function () {
- mapping.loadMappings({
- index: {
- properties: {
- first_name: {
- type: 'string',
- index: 'analyzed',
- path: 'just_name',
- fields: {
- any_name: { type: 'string', index: 'analyzed' },
- },
- },
- last_name: {
- type: 'string',
- index: 'no',
- fields: {
- raw: { type: 'string', index: 'analyzed' },
+ describe('When fields autocomplete is disabled', () => {
+ beforeEach(() => {
+ settingsMock.getAutocomplete.mockReturnValue({ fields: false });
+ });
+
+ test('does not return any suggestions', function () {
+ mapping.loadMappings({
+ index: {
+ properties: {
+ first_name: {
+ type: 'string',
+ index: 'analyzed',
+ path: 'just_name',
+ fields: {
+ any_name: { type: 'string', index: 'analyzed' },
+ },
},
},
},
- },
- });
+ });
- expect(mapping.getMappings('index').sort(fc)).toEqual([
- f('any_name', 'string'),
- f('first_name', 'string'),
- f('last_name', 'string'),
- f('last_name.raw', 'string'),
- ]);
+ expect(mapping.getMappings('index').sort(fc)).toEqual([]);
+ });
});
- test('Simple fields', function () {
- mapping.loadMappings({
- index: {
- properties: {
- str: {
- type: 'string',
- },
- number: {
- type: 'int',
+ describe('When fields autocomplete is enabled', () => {
+ beforeEach(() => {
+ settingsMock.getAutocomplete.mockReturnValue({ fields: true });
+ httpMock.get.mockReturnValue(
+ Promise.resolve({
+ mappings: { index: { mappings: { properties: { '@timestamp': { type: 'date' } } } } },
+ })
+ );
+ });
+
+ test('attempts to fetch mappings if not loaded', async () => {
+ const autoCompleteContext = {};
+ let loadingIndicator;
+
+ mapping.isLoading$.subscribe((v) => {
+ loadingIndicator = v;
+ });
+
+ // act
+ mapping.getMappings('index', [], autoCompleteContext);
+
+ expect(autoCompleteContext.asyncResultsState.isLoading).toBe(true);
+ expect(loadingIndicator).toBe(true);
+
+ expect(httpMock.get).toHaveBeenCalled();
+
+ const fields = await autoCompleteContext.asyncResultsState.results;
+
+ expect(loadingIndicator).toBe(false);
+ expect(autoCompleteContext.asyncResultsState.isLoading).toBe(false);
+ expect(fields).toEqual([{ name: '@timestamp', type: 'date' }]);
+ });
+
+ test('Multi fields 1.0 style', function () {
+ mapping.loadMappings({
+ index: {
+ properties: {
+ first_name: {
+ type: 'string',
+ index: 'analyzed',
+ path: 'just_name',
+ fields: {
+ any_name: { type: 'string', index: 'analyzed' },
+ },
+ },
+ last_name: {
+ type: 'string',
+ index: 'no',
+ fields: {
+ raw: { type: 'string', index: 'analyzed' },
+ },
+ },
},
},
- },
- });
+ });
- expect(mapping.getMappings('index').sort(fc)).toEqual([
- f('number', 'int'),
- f('str', 'string'),
- ]);
- });
+ expect(mapping.getMappings('index').sort(fc)).toEqual([
+ f('any_name', 'string'),
+ f('first_name', 'string'),
+ f('last_name', 'string'),
+ f('last_name.raw', 'string'),
+ ]);
+ });
- test('Simple fields - 1.0 style', function () {
- mapping.loadMappings({
- index: {
- mappings: {
+ test('Simple fields', function () {
+ mapping.loadMappings({
+ index: {
properties: {
str: {
type: 'string',
@@ -112,108 +162,130 @@ describe('Autocomplete entities', () => {
},
},
},
- },
- });
+ });
- expect(mapping.getMappings('index').sort(fc)).toEqual([
- f('number', 'int'),
- f('str', 'string'),
- ]);
- });
+ expect(mapping.getMappings('index').sort(fc)).toEqual([
+ f('number', 'int'),
+ f('str', 'string'),
+ ]);
+ });
- test('Nested fields', function () {
- mapping.loadMappings({
- index: {
- properties: {
- person: {
- type: 'object',
+ test('Simple fields - 1.0 style', function () {
+ mapping.loadMappings({
+ index: {
+ mappings: {
properties: {
- name: {
- properties: {
- first_name: { type: 'string' },
- last_name: { type: 'string' },
- },
+ str: {
+ type: 'string',
+ },
+ number: {
+ type: 'int',
},
- sid: { type: 'string', index: 'not_analyzed' },
},
},
- message: { type: 'string' },
},
- },
- });
+ });
- expect(mapping.getMappings('index', []).sort(fc)).toEqual([
- f('message'),
- f('person.name.first_name'),
- f('person.name.last_name'),
- f('person.sid'),
- ]);
- });
+ expect(mapping.getMappings('index').sort(fc)).toEqual([
+ f('number', 'int'),
+ f('str', 'string'),
+ ]);
+ });
- test('Enabled fields', function () {
- mapping.loadMappings({
- index: {
- properties: {
- person: {
- type: 'object',
- properties: {
- name: {
- type: 'object',
- enabled: false,
+ test('Nested fields', function () {
+ mapping.loadMappings({
+ index: {
+ properties: {
+ person: {
+ type: 'object',
+ properties: {
+ name: {
+ properties: {
+ first_name: { type: 'string' },
+ last_name: { type: 'string' },
+ },
+ },
+ sid: { type: 'string', index: 'not_analyzed' },
},
- sid: { type: 'string', index: 'not_analyzed' },
},
+ message: { type: 'string' },
},
- message: { type: 'string' },
},
- },
- });
+ });
- expect(mapping.getMappings('index', []).sort(fc)).toEqual([f('message'), f('person.sid')]);
- });
+ expect(mapping.getMappings('index', []).sort(fc)).toEqual([
+ f('message'),
+ f('person.name.first_name'),
+ f('person.name.last_name'),
+ f('person.sid'),
+ ]);
+ });
- test('Path tests', function () {
- mapping.loadMappings({
- index: {
- properties: {
- name1: {
- type: 'object',
- path: 'just_name',
- properties: {
- first1: { type: 'string' },
- last1: { type: 'string', index_name: 'i_last_1' },
+ test('Enabled fields', function () {
+ mapping.loadMappings({
+ index: {
+ properties: {
+ person: {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'object',
+ enabled: false,
+ },
+ sid: { type: 'string', index: 'not_analyzed' },
+ },
},
+ message: { type: 'string' },
},
- name2: {
- type: 'object',
- path: 'full',
- properties: {
- first2: { type: 'string' },
- last2: { type: 'string', index_name: 'i_last_2' },
+ },
+ });
+
+ expect(mapping.getMappings('index', []).sort(fc)).toEqual([f('message'), f('person.sid')]);
+ });
+
+ test('Path tests', function () {
+ mapping.loadMappings({
+ index: {
+ properties: {
+ name1: {
+ type: 'object',
+ path: 'just_name',
+ properties: {
+ first1: { type: 'string' },
+ last1: { type: 'string', index_name: 'i_last_1' },
+ },
+ },
+ name2: {
+ type: 'object',
+ path: 'full',
+ properties: {
+ first2: { type: 'string' },
+ last2: { type: 'string', index_name: 'i_last_2' },
+ },
},
},
},
- },
- });
+ });
- expect(mapping.getMappings().sort(fc)).toEqual([
- f('first1'),
- f('i_last_1'),
- f('name2.first2'),
- f('name2.i_last_2'),
- ]);
- });
+ expect(mapping.getMappings().sort(fc)).toEqual([
+ f('first1'),
+ f('i_last_1'),
+ f('name2.first2'),
+ f('name2.i_last_2'),
+ ]);
+ });
- test('Use index_name tests', function () {
- mapping.loadMappings({
- index: {
- properties: {
- last1: { type: 'string', index_name: 'i_last_1' },
+ test('Use index_name tests', function () {
+ mapping.loadMappings({
+ index: {
+ properties: {
+ last1: { type: 'string', index_name: 'i_last_1' },
+ },
},
- },
- });
+ });
- expect(mapping.getMappings().sort(fc)).toEqual([f('i_last_1')]);
+ expect(mapping.getMappings().sort(fc)).toEqual([f('i_last_1')]);
+ });
});
});
diff --git a/src/plugins/console/public/lib/autocomplete_entities/mapping.ts b/src/plugins/console/public/lib/autocomplete_entities/mapping.ts
index 71e72dac0a280..3c383ca124167 100644
--- a/src/plugins/console/public/lib/autocomplete_entities/mapping.ts
+++ b/src/plugins/console/public/lib/autocomplete_entities/mapping.ts
@@ -7,9 +7,15 @@
*/
import _ from 'lodash';
+import { BehaviorSubject } from 'rxjs';
import type { IndicesGetMappingResponse } from '@elastic/elasticsearch/lib/api/types';
+import { HttpSetup } from '@kbn/core-http-browser';
+import { type Settings } from '../../services';
+import { API_BASE_PATH } from '../../../common/constants';
+import type { ResultTerm, AutoCompleteContext } from '../autocomplete/types';
import { expandAliases } from './expand_aliases';
import type { Field, FieldMapping } from './types';
+import { type AutoCompleteEntitiesApiResponse } from './types';
function getFieldNamesFromProperties(properties: Record = {}) {
const fieldList = Object.entries(properties).flatMap(([fieldName, fieldMapping]) => {
@@ -70,22 +76,127 @@ function getFieldNamesFromFieldMapping(
export interface BaseMapping {
perIndexTypes: Record;
- getMappings(indices: string | string[], types?: string | string[]): Field[];
+ /**
+ * Fetches mappings definition
+ */
+ fetchMappings(index: string): Promise;
+
+ /**
+ * Retrieves mappings definition from cache, fetches if necessary.
+ */
+ getMappings(
+ indices: string | string[],
+ types?: string | string[],
+ autoCompleteContext?: AutoCompleteContext
+ ): Field[];
+
+ /**
+ * Stores mappings definition
+ * @param mappings
+ */
loadMappings(mappings: IndicesGetMappingResponse): void;
clearMappings(): void;
}
export class Mapping implements BaseMapping {
+ private http!: HttpSetup;
+
+ private settings!: Settings;
+
+ /**
+ * Map of the mappings of actual ES indices.
+ */
public perIndexTypes: Record = {};
- getMappings = (indices: string | string[], types?: string | string[]) => {
+ private readonly _isLoading$ = new BehaviorSubject(false);
+
+ /**
+ * Indicates if mapping fetching is in progress.
+ */
+ public readonly isLoading$ = this._isLoading$.asObservable();
+
+ /**
+ * Map of the currently loading mappings for index patterns specified by a user.
+ * @private
+ */
+ private loadingState: Record = {};
+
+ public setup(http: HttpSetup, settings: Settings) {
+ this.http = http;
+ this.settings = settings;
+ }
+
+ /**
+ * Fetches mappings of the requested indices.
+ * @param index
+ */
+ async fetchMappings(index: string): Promise {
+ const response = await this.http.get(
+ `${API_BASE_PATH}/autocomplete_entities`,
+ {
+ query: { fields: true, fieldsIndices: index },
+ }
+ );
+
+ return response.mappings;
+ }
+
+ getMappings = (
+ indices: string | string[],
+ types?: string | string[],
+ autoCompleteContext?: AutoCompleteContext
+ ) => {
// get fields for indices and types. Both can be a list, a string or null (meaning all).
let ret: Field[] = [];
+
+ if (!this.settings.getAutocomplete().fields) return ret;
+
indices = expandAliases(indices);
if (typeof indices === 'string') {
const typeDict = this.perIndexTypes[indices] as Record;
- if (!typeDict) {
+
+ if (!typeDict || Object.keys(typeDict).length === 0) {
+ if (!autoCompleteContext) return ret;
+
+ // Mappings fetching for the index is already in progress
+ if (this.loadingState[indices]) return ret;
+
+ this.loadingState[indices] = true;
+
+ if (!autoCompleteContext.asyncResultsState) {
+ autoCompleteContext.asyncResultsState = {} as AutoCompleteContext['asyncResultsState'];
+ }
+
+ autoCompleteContext.asyncResultsState!.isLoading = true;
+
+ autoCompleteContext.asyncResultsState!.results = new Promise(
+ (resolve, reject) => {
+ this._isLoading$.next(true);
+
+ this.fetchMappings(indices as string)
+ .then((mapping) => {
+ this._isLoading$.next(false);
+
+ autoCompleteContext.asyncResultsState!.isLoading = false;
+ autoCompleteContext.asyncResultsState!.lastFetched = Date.now();
+
+ // cache mappings
+ this.loadMappings(mapping);
+
+ const mappings = this.getMappings(indices, types, autoCompleteContext);
+ delete this.loadingState[indices as string];
+ resolve(mappings);
+ })
+ .catch((error) => {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ this._isLoading$.next(false);
+ delete this.loadingState[indices as string];
+ });
+ }
+ );
+
return [];
}
@@ -108,7 +219,7 @@ export class Mapping implements BaseMapping {
// multi index mode.
Object.keys(this.perIndexTypes).forEach((index) => {
if (!indices || indices.length === 0 || indices.includes(index)) {
- ret.push(this.getMappings(index, types) as unknown as Field);
+ ret.push(this.getMappings(index, types, autoCompleteContext) as unknown as Field);
}
});
@@ -121,8 +232,6 @@ export class Mapping implements BaseMapping {
};
loadMappings = (mappings: IndicesGetMappingResponse) => {
- this.perIndexTypes = {};
-
Object.entries(mappings).forEach(([index, indexMapping]) => {
const normalizedIndexMappings: Record = {};
let transformedMapping: Record = indexMapping;
diff --git a/src/plugins/console/public/services/autocomplete.ts b/src/plugins/console/public/services/autocomplete.ts
index 3e1a38a514607..57324049bab94 100644
--- a/src/plugins/console/public/services/autocomplete.ts
+++ b/src/plugins/console/public/services/autocomplete.ts
@@ -53,7 +53,11 @@ export class AutocompleteInfo {
const collaborator = this.mapping;
return () => this.alias.getIndices(includeAliases, collaborator);
case ENTITIES.FIELDS:
- return this.mapping.getMappings(context.indices, context.types);
+ return this.mapping.getMappings(
+ context.indices,
+ context.types,
+ Object.getPrototypeOf(context)
+ );
case ENTITIES.INDEX_TEMPLATES:
return () => this.indexTemplate.getTemplates();
case ENTITIES.COMPONENT_TEMPLATES:
@@ -93,7 +97,6 @@ export class AutocompleteInfo {
}
private load(data: AutoCompleteEntitiesApiResponse) {
- this.mapping.loadMappings(data.mappings);
const collaborator = this.mapping;
this.alias.loadAliases(data.aliases, collaborator);
this.indexTemplate.loadTemplates(data.indexTemplates);
diff --git a/src/plugins/console/public/services/settings.ts b/src/plugins/console/public/services/settings.ts
index e4731dd3f3a31..4056d20063a3e 100644
--- a/src/plugins/console/public/services/settings.ts
+++ b/src/plugins/console/public/services/settings.ts
@@ -14,7 +14,12 @@ export const DEFAULT_SETTINGS = Object.freeze({
pollInterval: 60000,
tripleQuotes: true,
wrapMode: true,
- autocomplete: Object.freeze({ fields: true, indices: true, templates: true, dataStreams: true }),
+ autocomplete: Object.freeze({
+ fields: true,
+ indices: true,
+ templates: true,
+ dataStreams: true,
+ }),
isHistoryEnabled: true,
isKeyboardShortcutsEnabled: true,
});
diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss
index 2490bb29f0fb7..9cbe643722047 100644
--- a/src/plugins/console/public/styles/_app.scss
+++ b/src/plugins/console/public/styles/_app.scss
@@ -30,7 +30,7 @@
}
}
-.conApp__output {
+.conApp__output, .conApp_example {
display: flex;
flex: 1 1 1px;
diff --git a/src/plugins/console/public/styles/components/_help.scss b/src/plugins/console/public/styles/components/_help.scss
deleted file mode 100644
index 594bcfd1db311..0000000000000
--- a/src/plugins/console/public/styles/components/_help.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.conHelp__example {
- display: block;
- height: $euiSize * 8; // 8 lines of code
- margin: $euiSize 0;
-}
diff --git a/src/plugins/console/public/styles/components/_index.scss b/src/plugins/console/public/styles/components/_index.scss
index 9dfef202d1254..0af86f54f723f 100644
--- a/src/plugins/console/public/styles/components/_index.scss
+++ b/src/plugins/console/public/styles/components/_index.scss
@@ -1,2 +1 @@
-@import 'help';
@import 'history';
diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts
index 1c9d6352914a2..34f44578a0bce 100644
--- a/src/plugins/console/public/types/core_editor.ts
+++ b/src/plugins/console/public/types/core_editor.ts
@@ -7,6 +7,7 @@
*/
import type { Editor } from 'brace';
+import { ResultTerm } from '../lib/autocomplete/types';
import { TokensProvider } from './tokens_provider';
import { Token } from './token';
@@ -23,7 +24,11 @@ export type EditorEvent =
export type AutoCompleterFunction = (
pos: Position,
prefix: string,
- callback: (...args: unknown[]) => void
+ callback: (e: Error | null, result: ResultTerm[] | null) => void,
+ annotationControls: {
+ setAnnotation: (text: string) => void;
+ removeAnnotation: () => void;
+ }
) => void;
export interface Position {
diff --git a/src/plugins/console/server/routes/api/console/autocomplete_entities/index.ts b/src/plugins/console/server/routes/api/console/autocomplete_entities/index.ts
index 8bd5f8ee50b48..2d19de0a56e74 100644
--- a/src/plugins/console/server/routes/api/console/autocomplete_entities/index.ts
+++ b/src/plugins/console/server/routes/api/console/autocomplete_entities/index.ts
@@ -6,26 +6,25 @@
* Side Public License, v 1.
*/
-import { parse } from 'query-string';
import type { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import type { RouteDependencies } from '../../..';
-interface SettingsToRetrieve {
- indices: boolean;
- fields: boolean;
- templates: boolean;
- dataStreams: boolean;
-}
+import { autoCompleteEntitiesValidationConfig, type SettingsToRetrieve } from './validation_config';
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB
// Limit the response size to 10MB, because the response can be very large and sending it to the client
// can cause the browser to hang.
const getMappings = async (settings: SettingsToRetrieve, esClient: IScopedClusterClient) => {
- if (settings.fields) {
- const mappings = await esClient.asInternalUser.indices.getMapping(undefined, {
- maxResponseSize: MAX_RESPONSE_SIZE,
- maxCompressedResponseSize: MAX_RESPONSE_SIZE,
- });
+ if (settings.fields && settings.fieldsIndices) {
+ const mappings = await esClient.asInternalUser.indices.getMapping(
+ {
+ index: settings.fieldsIndices,
+ },
+ {
+ maxResponseSize: MAX_RESPONSE_SIZE,
+ maxCompressedResponseSize: MAX_RESPONSE_SIZE,
+ }
+ );
return mappings;
}
// If the user doesn't want autocomplete suggestions, then clear any that exist.
@@ -87,20 +86,11 @@ export const registerAutocompleteEntitiesRoute = (deps: RouteDependencies) => {
options: {
tags: ['access:console'],
},
- validate: false,
+ validate: autoCompleteEntitiesValidationConfig,
},
async (context, request, response) => {
const esClient = (await context.core).elasticsearch.client;
- const settings = parse(request.url.search, {
- parseBooleans: true,
- }) as unknown as SettingsToRetrieve;
-
- // If no settings are specified, then return 400.
- if (Object.keys(settings).length === 0) {
- return response.badRequest({
- body: 'Request must contain at least one of the following parameters: indices, fields, templates, dataStreams',
- });
- }
+ const settings = request.query;
// Wait for all requests to complete, in case one of them fails return the successfull ones
const results = await Promise.allSettled([
diff --git a/src/plugins/console/server/routes/api/console/autocomplete_entities/validation_config.ts b/src/plugins/console/server/routes/api/console/autocomplete_entities/validation_config.ts
new file mode 100644
index 0000000000000..48bab100d0b61
--- /dev/null
+++ b/src/plugins/console/server/routes/api/console/autocomplete_entities/validation_config.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { schema, TypeOf } from '@kbn/config-schema';
+
+export const autoCompleteEntitiesValidationConfig = {
+ query: schema.object(
+ {
+ indices: schema.maybe(schema.boolean()),
+ fields: schema.maybe(schema.boolean()),
+ templates: schema.maybe(schema.boolean()),
+ dataStreams: schema.maybe(schema.boolean()),
+ /**
+ * Comma separated list of indices for mappings retrieval.
+ */
+ fieldsIndices: schema.maybe(schema.string()),
+ },
+ {
+ validate: (payload) => {
+ if (Object.keys(payload).length === 0) {
+ return 'The request must contain at least one of the following parameters: indices, fields, templates, dataStreams.';
+ }
+ },
+ }
+ ),
+};
+
+export type SettingsToRetrieve = TypeOf;
diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json
index 0dcc23b1c060c..43b94e47eedd4 100644
--- a/src/plugins/console/tsconfig.json
+++ b/src/plugins/console/tsconfig.json
@@ -30,6 +30,7 @@
"@kbn/core-http-router-server-internal",
"@kbn/web-worker-stub",
"@kbn/core-elasticsearch-server",
+ "@kbn/core-http-browser-mocks",
],
"exclude": [
"target/**/*",
diff --git a/src/plugins/data_views/common/fields/fields.mocks.ts b/src/plugins/data_views/common/fields/fields.mocks.ts
index 05a51d7ccf120..bee37c3eb9761 100644
--- a/src/plugins/data_views/common/fields/fields.mocks.ts
+++ b/src/plugins/data_views/common/fields/fields.mocks.ts
@@ -306,6 +306,29 @@ export const fields: FieldSpec[] = [
readFromDocValues: false,
subType: { nested: { path: 'nestedField.nestedChild' } },
},
+ {
+ name: 'mapping issues',
+ type: 'conflict',
+ esTypes: ['text', 'unmapped'],
+ count: 0,
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ conflictDescriptions: {
+ text: [
+ '.siem-signals-default-000001',
+ '.siem-signals-default-000002',
+ '.siem-signals-default-000011',
+ '.siem-signals-default-000012',
+ ],
+ unmapped: [
+ '.siem-signals-default-000004',
+ '.siem-signals-default-000005',
+ '.siem-signals-default-000240',
+ ],
+ },
+ },
];
export const getField = (name: string) => fields.find((field) => field.name === name) as FieldSpec;
diff --git a/src/plugins/home/public/application/components/move_data/move_data.test.tsx b/src/plugins/home/public/application/components/move_data/move_data.test.tsx
index 9c92afbf01ac3..7f8e3a1bfcf7a 100644
--- a/src/plugins/home/public/application/components/move_data/move_data.test.tsx
+++ b/src/plugins/home/public/application/components/move_data/move_data.test.tsx
@@ -17,6 +17,6 @@ describe('MoveData', () => {
const component = shallowWithIntl( );
const $button = component.find('EuiButton');
- expect($button.props().href).toBe('/app/management/data/migrate_data');
+ expect($button.props().href).toBe('https://ela.st/cloud-migration');
});
});
diff --git a/src/plugins/home/public/application/components/move_data/move_data.tsx b/src/plugins/home/public/application/components/move_data/move_data.tsx
index ce97e71a6ecd8..c3e0181aa4f64 100644
--- a/src/plugins/home/public/application/components/move_data/move_data.tsx
+++ b/src/plugins/home/public/application/components/move_data/move_data.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import React, { FC, MouseEvent } from 'react';
+import React, { FC } from 'react';
import {
EuiButton,
EuiFlexGroup,
@@ -19,18 +19,17 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
-import { createAppNavigationHandler } from '../app_navigation_handler';
interface Props {
addBasePath: (path: string) => string;
}
export const MoveData: FC = ({ addBasePath }) => {
- const migrateDataUrl = '/app/management/data/migrate_data';
+ const migrateDataUrl = 'https://ela.st/cloud-migration';
const buttonLabel = (
);
@@ -50,7 +49,7 @@ export const MoveData: FC = ({ addBasePath }) => {
@@ -58,18 +57,15 @@ export const MoveData: FC = ({ addBasePath }) => {
- {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
{
- createAppNavigationHandler(migrateDataUrl)(event);
- }}
+ href={migrateDataUrl}
+ target="_blank"
>
{buttonLabel}
diff --git a/test/api_integration/apis/console/autocomplete_entities.ts b/test/api_integration/apis/console/autocomplete_entities.ts
index 6bd899c979a2b..6e13b2fb2856b 100644
--- a/test/api_integration/apis/console/autocomplete_entities.ts
+++ b/test/api_integration/apis/console/autocomplete_entities.ts
@@ -128,7 +128,7 @@ export default ({ getService }: FtrProviderContext) => {
return await supertest.get('/api/console/autocomplete_entities').query(query);
};
- describe('/api/console/autocomplete_entities', () => {
+ describe('/api/console/autocomplete_entities', function () {
const indexName = 'test-index-1';
const aliasName = 'test-alias-1';
const indexTemplateName = 'test-index-template-1';
@@ -238,9 +238,17 @@ export default ({ getService }: FtrProviderContext) => {
expect(body.mappings).to.eql({});
});
- it('should return mappings with fields setting is set to true', async () => {
+ it('should not return mappings with fields setting is set to true without the list of indices is provided', async () => {
const response = await sendRequest({ fields: true });
+ const { body, status } = response;
+ expect(status).to.be(200);
+ expect(Object.keys(body.mappings)).to.not.contain(indexName);
+ });
+
+ it('should return mappings with fields setting is set to true and the list of indices is provided', async () => {
+ const response = await sendRequest({ fields: true, fieldsIndices: indexName });
+
const { body, status } = response;
expect(status).to.be(200);
expect(Object.keys(body.mappings)).to.contain(indexName);
diff --git a/x-pack/plugins/aiops/common/__mocks__/artificial_logs/filtered_frequent_items.ts b/x-pack/plugins/aiops/common/__mocks__/artificial_logs/filtered_frequent_item_sets.ts
similarity index 92%
rename from x-pack/plugins/aiops/common/__mocks__/artificial_logs/filtered_frequent_items.ts
rename to x-pack/plugins/aiops/common/__mocks__/artificial_logs/filtered_frequent_item_sets.ts
index 268516f95542d..5f3d8ce759e19 100644
--- a/x-pack/plugins/aiops/common/__mocks__/artificial_logs/filtered_frequent_items.ts
+++ b/x-pack/plugins/aiops/common/__mocks__/artificial_logs/filtered_frequent_item_sets.ts
@@ -7,7 +7,7 @@
import type { ItemsetResult } from '../../types';
-export const filteredFrequentItems: ItemsetResult[] = [
+export const filteredFrequentItemSets: ItemsetResult[] = [
{
set: { response_code: '500', url: 'home.php' },
size: 2,
diff --git a/x-pack/plugins/aiops/common/__mocks__/artificial_logs/frequent_items.ts b/x-pack/plugins/aiops/common/__mocks__/artificial_logs/frequent_item_sets.ts
similarity index 96%
rename from x-pack/plugins/aiops/common/__mocks__/artificial_logs/frequent_items.ts
rename to x-pack/plugins/aiops/common/__mocks__/artificial_logs/frequent_item_sets.ts
index fe61a60a1afbe..783ea7d674d3d 100644
--- a/x-pack/plugins/aiops/common/__mocks__/artificial_logs/frequent_items.ts
+++ b/x-pack/plugins/aiops/common/__mocks__/artificial_logs/frequent_item_sets.ts
@@ -7,7 +7,7 @@
import type { ItemsetResult } from '../../types';
-export const frequentItems: ItemsetResult[] = [
+export const frequentItemSets: ItemsetResult[] = [
{
set: { response_code: '500', url: 'home.php' },
size: 2,
diff --git a/x-pack/plugins/aiops/public/application/utils/search_utils.ts b/x-pack/plugins/aiops/public/application/utils/search_utils.ts
index 3bd0ea7a96523..f8f570d475fc0 100644
--- a/x-pack/plugins/aiops/public/application/utils/search_utils.ts
+++ b/x-pack/plugins/aiops/public/application/utils/search_utils.ts
@@ -11,8 +11,8 @@
import { cloneDeep } from 'lodash';
import { IUiSettingsClient } from '@kbn/core/public';
import { getEsQueryConfig, SearchSource } from '@kbn/data-plugin/common';
-import { SavedSearch } from '@kbn/discover-plugin/public';
-import { FilterManager } from '@kbn/data-plugin/public';
+import type { SavedSearch } from '@kbn/discover-plugin/public';
+import { FilterManager, isQuery, mapAndFlattenFilters } from '@kbn/data-plugin/public';
import {
fromKueryExpression,
toElasticsearchQuery,
@@ -20,6 +20,7 @@ import {
buildEsQuery,
Query,
Filter,
+ AggregateQuery,
} from '@kbn/es-query';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { DataView } from '@kbn/data-views-plugin/public';
@@ -88,14 +89,15 @@ export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObjec
* Should also form a valid query if only the query or filters is provided
*/
export function createMergedEsQuery(
- query?: Query,
+ query?: Query | AggregateQuery,
filters?: Filter[],
dataView?: DataView,
uiSettings?: IUiSettingsClient
) {
let combinedQuery: QueryDslQueryContainer = getDefaultQuery();
- if (query && query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
+ // FIXME: Add support for AggregateQuery type #150091
+ if (isQuery(query) && query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
const ast = fromKueryExpression(query.query);
if (query.query !== '') {
combinedQuery = toElasticsearchQuery(ast, dataView);
@@ -127,6 +129,14 @@ export function createMergedEsQuery(
return combinedQuery;
}
+function getSavedSearchSource(savedSearch: SavedSearch) {
+ return savedSearch &&
+ 'searchSource' in savedSearch &&
+ savedSearch?.searchSource instanceof SearchSource
+ ? savedSearch.searchSource
+ : undefined;
+}
+
/**
* Extract query data from the saved search object
* with overrides from the provided query data and/or filters
@@ -141,7 +151,7 @@ export function getEsQueryFromSavedSearch({
}: {
dataView: DataView;
uiSettings: IUiSettingsClient;
- savedSearch: SavedSearchSavedObject | SavedSearch | null | undefined;
+ savedSearch: SavedSearch | null | undefined;
query?: Query;
filters?: Filter[];
filterManager?: FilterManager;
@@ -151,17 +161,13 @@ export function getEsQueryFromSavedSearch({
const userQuery = query;
const userFilters = filters;
+ const savedSearchSource = getSavedSearchSource(savedSearch);
+
// If saved search has a search source with nested parent
// e.g. a search coming from Dashboard saved search embeddable
// which already combines both the saved search's original query/filters and the Dashboard's
// then no need to process any further
- if (
- savedSearch &&
- 'searchSource' in savedSearch &&
- savedSearch?.searchSource instanceof SearchSource &&
- savedSearch.searchSource.getParent() !== undefined &&
- userQuery
- ) {
+ if (savedSearchSource && savedSearchSource.getParent() !== undefined && userQuery) {
// Flattened query from search source may contain a clause that narrows the time range
// which might interfere with global time pickers so we need to remove
const savedQuery =
@@ -181,12 +187,8 @@ export function getEsQueryFromSavedSearch({
};
}
- // If saved search is an json object with the original query and filter
- // retrieve the parsed query and filter
- const savedSearchData = getQueryFromSavedSearchObject(savedSearch);
-
// If no saved search available, use user's query and filters
- if (!savedSearchData && userQuery) {
+ if (!savedSearch && userQuery) {
if (filterManager && userFilters) filterManager.addFilters(userFilters);
const combinedQuery = createMergedEsQuery(
@@ -205,11 +207,12 @@ export function getEsQueryFromSavedSearch({
// If saved search available, merge saved search with the latest user query or filters
// which might differ from extracted saved search data
- if (savedSearchData) {
+ if (savedSearchSource) {
const globalFilters = filterManager?.getGlobalFilters();
- const currentQuery = userQuery ?? savedSearchData?.query;
- const currentFilters = userFilters ?? savedSearchData?.filter;
-
+ // FIXME: Add support for AggregateQuery type #150091
+ const currentQuery = userQuery ?? (savedSearchSource.getField('query') as Query);
+ const currentFilters =
+ userFilters ?? mapAndFlattenFilters(savedSearchSource.getField('filter') as Filter[]);
if (filterManager) filterManager.setFilters(currentFilters);
if (globalFilters) filterManager?.addFilters(globalFilters);
diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detetion_root.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detetion_root.tsx
index fb9dbc5156ed4..d2d275f0b2251 100644
--- a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detetion_root.tsx
+++ b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detetion_root.tsx
@@ -11,7 +11,7 @@ import { pick } from 'lodash';
import { EuiSpacer } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/common';
-import { SavedSearch } from '@kbn/saved-search-plugin/public';
+import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { Storage } from '@kbn/kibana-utils-plugin/public';
@@ -20,7 +20,6 @@ import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { DataSourceContext } from '../../hooks/use_data_source';
-import { SavedSearchSavedObject } from '../../application/utils/search_utils';
import { AiopsAppContext, AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
@@ -33,7 +32,7 @@ const localStorage = new Storage(window.localStorage);
export interface ChangePointDetectionAppStateProps {
dataView: DataView;
- savedSearch: SavedSearch | SavedSearchSavedObject | null;
+ savedSearch: SavedSearch | null;
appDependencies: AiopsAppDependencies;
}
diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx
index ae67c55d87f05..f43b60d7daffa 100644
--- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx
+++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx
@@ -21,11 +21,7 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
-import {
- SEARCH_QUERY_LANGUAGE,
- SearchQueryLanguage,
- SavedSearchSavedObject,
-} from '../../application/utils/search_utils';
+import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../application/utils/search_utils';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import { DataSourceContext } from '../../hooks/use_data_source';
@@ -41,7 +37,7 @@ export interface ExplainLogRateSpikesAppStateProps {
/** The data view to analyze. */
dataView: DataView;
/** The saved search to analyze. */
- savedSearch: SavedSearch | SavedSearchSavedObject | null;
+ savedSearch: SavedSearch | null;
/** App dependencies */
appDependencies: AiopsAppDependencies;
}
diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx
index 8f4504070cc08..6c277456d2ec3 100644
--- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx
+++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx
@@ -67,11 +67,11 @@ export const ExplainLogRateSpikesPage: FC = () => {
);
const [globalState, setGlobalState] = useUrlState('_g');
- const [currentSavedSearch, setCurrentSavedSearch] = useState(savedSearch);
+ const [selectedSavedSearch, setSelectedSavedSearch] = useState(savedSearch);
useEffect(() => {
if (savedSearch) {
- setCurrentSavedSearch(savedSearch);
+ setSelectedSavedSearch(savedSearch);
}
}, [savedSearch]);
@@ -84,8 +84,8 @@ export const ExplainLogRateSpikesPage: FC = () => {
}) => {
// When the user loads a saved search and then clears or modifies the query
// we should remove the saved search and replace it with the index pattern id
- if (currentSavedSearch !== null) {
- setCurrentSavedSearch(null);
+ if (selectedSavedSearch !== null) {
+ setSelectedSavedSearch(null);
}
setAiopsListState({
@@ -96,7 +96,7 @@ export const ExplainLogRateSpikesPage: FC = () => {
filters: searchParams.filters,
});
},
- [currentSavedSearch, aiopsListState, setAiopsListState]
+ [selectedSavedSearch, aiopsListState, setAiopsListState]
);
const {
@@ -108,7 +108,7 @@ export const ExplainLogRateSpikesPage: FC = () => {
searchString,
searchQuery,
} = useData(
- { currentDataView: dataView, currentSavedSearch },
+ { selectedDataView: dataView, selectedSavedSearch },
aiopsListState,
setGlobalState,
currentSelectedChangePoint,
diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx
index 235aa1a518403..b554013393926 100644
--- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx
+++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx
@@ -17,7 +17,6 @@ import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { DataSourceContext } from '../../hooks/use_data_source';
-import { SavedSearchSavedObject } from '../../application/utils/search_utils';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
@@ -28,7 +27,7 @@ const localStorage = new Storage(window.localStorage);
export interface LogCategorizationAppStateProps {
dataView: DataView;
- savedSearch: SavedSearch | SavedSearchSavedObject | null;
+ savedSearch: SavedSearch | null;
appDependencies: AiopsAppDependencies;
}
diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx
index ab66a75754e63..feb6ef9c9b31c 100644
--- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx
+++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx
@@ -52,13 +52,19 @@ export const LogCategorizationPage: FC = () => {
const [selectedField, setSelectedField] = useState();
const [selectedCategory, setSelectedCategory] = useState(null);
const [categories, setCategories] = useState(null);
- const [currentSavedSearch, setCurrentSavedSearch] = useState(savedSearch);
+ const [selectedSavedSearch, setSelectedDataView] = useState(savedSearch);
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);
const [eventRate, setEventRate] = useState([]);
const [pinnedCategory, setPinnedCategory] = useState(null);
const [sparkLines, setSparkLines] = useState({});
+ useEffect(() => {
+ if (savedSearch) {
+ setSelectedDataView(savedSearch);
+ }
+ }, [savedSearch]);
+
useEffect(
function cancelRequestOnLeave() {
return () => {
@@ -77,8 +83,8 @@ export const LogCategorizationPage: FC = () => {
}) => {
// When the user loads saved search and then clear or modify the query
// we should remove the saved search and replace it with the index pattern id
- if (currentSavedSearch !== null) {
- setCurrentSavedSearch(null);
+ if (selectedSavedSearch !== null) {
+ setSelectedDataView(null);
}
setAiopsListState({
@@ -89,7 +95,7 @@ export const LogCategorizationPage: FC = () => {
filters: searchParams.filters,
});
},
- [currentSavedSearch, aiopsListState, setAiopsListState]
+ [selectedSavedSearch, aiopsListState, setAiopsListState]
);
const {
@@ -102,7 +108,7 @@ export const LogCategorizationPage: FC = () => {
searchQuery,
intervalMs,
} = useData(
- { currentDataView: dataView, currentSavedSearch },
+ { selectedDataView: dataView, selectedSavedSearch },
aiopsListState,
setGlobalState,
undefined,
diff --git a/x-pack/plugins/aiops/public/hooks/use_data.ts b/x-pack/plugins/aiops/public/hooks/use_data.ts
index 3cace69ed4e98..ff04b27b44e3c 100644
--- a/x-pack/plugins/aiops/public/hooks/use_data.ts
+++ b/x-pack/plugins/aiops/public/hooks/use_data.ts
@@ -16,10 +16,7 @@ import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import type { DocumentStatsSearchStrategyParams } from '../get_document_stats';
import type { AiOpsIndexBasedAppState } from '../components/explain_log_rate_spikes/explain_log_rate_spikes_app_state';
-import {
- getEsQueryFromSavedSearch,
- SavedSearchSavedObject,
-} from '../application/utils/search_utils';
+import { getEsQueryFromSavedSearch } from '../application/utils/search_utils';
import type { GroupTableItem } from '../components/spike_analysis_table/types';
import { useTimeBuckets } from './use_time_buckets';
@@ -31,9 +28,9 @@ const DEFAULT_BAR_TARGET = 75;
export const useData = (
{
- currentDataView,
- currentSavedSearch,
- }: { currentDataView: DataView; currentSavedSearch: SavedSearch | SavedSearchSavedObject | null },
+ selectedDataView,
+ selectedSavedSearch,
+ }: { selectedDataView: DataView; selectedSavedSearch: SavedSearch | null },
aiopsListState: AiOpsIndexBasedAppState,
onUpdate: (params: Dictionary) => void,
selectedChangePoint?: ChangePoint,
@@ -55,9 +52,9 @@ export const useData = (
/** Prepare required params to pass to search strategy **/
const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
const searchData = getEsQueryFromSavedSearch({
- dataView: currentDataView,
+ dataView: selectedDataView,
uiSettings,
- savedSearch: currentSavedSearch,
+ savedSearch: selectedSavedSearch,
filterManager,
});
@@ -82,8 +79,8 @@ export const useData = (
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
- currentSavedSearch?.id,
- currentDataView.id,
+ selectedSavedSearch?.id,
+ selectedDataView.id,
aiopsListState.searchString,
aiopsListState.searchQueryLanguage,
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -96,7 +93,7 @@ export const useData = (
const _timeBuckets = useTimeBuckets();
const timefilter = useTimefilter({
- timeRangeSelector: currentDataView?.timeFieldName !== undefined,
+ timeRangeSelector: selectedDataView?.timeFieldName !== undefined,
autoRefreshSelector: true,
});
@@ -138,10 +135,10 @@ export const useData = (
earliest: timefilterActiveBounds.min?.valueOf(),
latest: timefilterActiveBounds.max?.valueOf(),
intervalMs: _timeBuckets.getInterval()?.asMilliseconds(),
- index: currentDataView.getIndexPattern(),
+ index: selectedDataView.getIndexPattern(),
searchQuery,
- timeFieldName: currentDataView.timeFieldName,
- runtimeFieldMap: currentDataView.getRuntimeMappings(),
+ timeFieldName: selectedDataView.timeFieldName,
+ runtimeFieldMap: selectedDataView.getRuntimeMappings(),
});
setLastRefresh(Date.now());
}
diff --git a/x-pack/plugins/aiops/public/hooks/use_data_source.ts b/x-pack/plugins/aiops/public/hooks/use_data_source.ts
index e67aad07a338c..64cf4fe8ec521 100644
--- a/x-pack/plugins/aiops/public/hooks/use_data_source.ts
+++ b/x-pack/plugins/aiops/public/hooks/use_data_source.ts
@@ -7,12 +7,11 @@
import { createContext, useContext } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
-import { SavedSearch } from '@kbn/saved-search-plugin/public';
-import { SavedSearchSavedObject } from '../application/utils/search_utils';
+import type { SavedSearch } from '@kbn/saved-search-plugin/public';
export const DataSourceContext = createContext<{
dataView: DataView | never;
- savedSearch: SavedSearch | SavedSearchSavedObject | null;
+ savedSearch: SavedSearch | null;
}>({
get dataView(): never {
throw new Error('DataSourceContext is not implemented');
diff --git a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts
index dfbc313632efc..f8fac77a63cd8 100644
--- a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts
+++ b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts
@@ -44,7 +44,7 @@ import type { AiopsLicense } from '../types';
import { duplicateIdentifier } from './queries/duplicate_identifier';
import { fetchChangePointPValues } from './queries/fetch_change_point_p_values';
import { fetchIndexInfo } from './queries/fetch_index_info';
-import { dropDuplicates, fetchFrequentItems } from './queries/fetch_frequent_items';
+import { dropDuplicates, fetchFrequentItemSets } from './queries/fetch_frequent_item_sets';
import { getHistogramQuery } from './queries/get_histogram_query';
import { getGroupFilter } from './queries/get_group_filter';
import { getChangePointGroups } from './queries/get_change_point_groups';
@@ -423,11 +423,11 @@ export const defineExplainLogRateSpikesRoute = (
})
);
- // Deduplicated change points we pass to the `frequent_items` aggregation.
+ // Deduplicated change points we pass to the `frequent_item_sets` aggregation.
const deduplicatedChangePoints = dropDuplicates(changePoints, duplicateIdentifier);
try {
- const { fields, df } = await fetchFrequentItems(
+ const { fields, df } = await fetchFrequentItemSets(
client,
request.body.index,
JSON.parse(request.body.searchQuery) as estypes.QueryDslQueryContainer,
@@ -442,7 +442,7 @@ export const defineExplainLogRateSpikesRoute = (
);
if (shouldStop) {
- logDebugMessage('shouldStop after fetching frequent_items.');
+ logDebugMessage('shouldStop after fetching frequent_item_sets.');
end();
return;
}
diff --git a/x-pack/plugins/aiops/server/routes/queries/duplicate_identifier.ts b/x-pack/plugins/aiops/server/routes/queries/duplicate_identifier.ts
index d996c060f7fe9..b8e24946672dc 100644
--- a/x-pack/plugins/aiops/server/routes/queries/duplicate_identifier.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/duplicate_identifier.ts
@@ -7,7 +7,7 @@
import type { ChangePoint } from '@kbn/ml-agg-utils';
-// To optimize the `frequent_items` query, we identify duplicate change points by count attributes.
+// To optimize the `frequent_item_sets` query, we identify duplicate change points by count attributes.
// Note this is a compromise and not 100% accurate because there could be change points that
// have the exact same counts but still don't co-occur.
export const duplicateIdentifier: Array = [
diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_item_sets.ts
similarity index 84%
rename from x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts
rename to x-pack/plugins/aiops/server/routes/queries/fetch_frequent_item_sets.ts
index 76aeeb1eabcdb..2f28a55ceeb2d 100644
--- a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_item_sets.ts
@@ -16,16 +16,16 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { ChangePointDuplicateGroup, ItemsetResult } from '../../../common/types';
-const FREQUENT_ITEMS_FIELDS_LIMIT = 15;
+const FREQUENT_ITEM_SETS_FIELDS_LIMIT = 15;
-interface FrequentItemsAggregation extends estypes.AggregationsSamplerAggregation {
+interface FrequentItemSetsAggregation extends estypes.AggregationsSamplerAggregation {
fi: {
buckets: Array<{ key: Record; doc_count: number; support: number }>;
};
}
interface RandomSamplerAggregation {
- sample: FrequentItemsAggregation;
+ sample: FrequentItemSetsAggregation;
}
function isRandomSamplerAggregation(arg: unknown): arg is RandomSamplerAggregation {
@@ -56,7 +56,7 @@ export function groupDuplicates(cps: ChangePoint[], uniqueFields: Array((p, c) => {
- if (p.length < FREQUENT_ITEMS_FIELDS_LIMIT && !p.some((d) => d === c.fieldName)) {
+ if (p.length < FREQUENT_ITEM_SETS_FIELDS_LIMIT && !p.some((d) => d === c.fieldName)) {
p.push(c.fieldName);
}
return p;
@@ -107,10 +107,10 @@ export async function fetchFrequentItems(
field,
}));
- const frequentItemsAgg: Record = {
+ const frequentItemSetsAgg: Record = {
fi: {
- // @ts-expect-error `frequent_items` is not yet part of `AggregationsAggregationContainer`
- frequent_items: {
+ // @ts-expect-error `frequent_item_sets` is not yet part of `AggregationsAggregationContainer`
+ frequent_item_sets: {
minimum_set_size: 2,
size: 200,
minimum_support: 0.1,
@@ -127,20 +127,20 @@ export async function fetchFrequentItems(
probability: sampleProbability,
seed: RANDOM_SAMPLER_SEED,
},
- aggs: frequentItemsAgg,
+ aggs: frequentItemSetsAgg,
},
};
const esBody = {
query,
- aggs: sampleProbability < 1 ? randomSamplerAgg : frequentItemsAgg,
+ aggs: sampleProbability < 1 ? randomSamplerAgg : frequentItemSetsAgg,
size: 0,
track_total_hits: true,
};
const body = await client.search<
unknown,
- { sample: FrequentItemsAggregation } | FrequentItemsAggregation
+ { sample: FrequentItemSetsAggregation } | FrequentItemSetsAggregation
>(
{
index,
@@ -151,8 +151,8 @@ export async function fetchFrequentItems(
);
if (body.aggregations === undefined) {
- logger.error(`Failed to fetch frequent_items, got: \n${JSON.stringify(body, null, 2)}`);
- emitError(`Failed to fetch frequent_items.`);
+ logger.error(`Failed to fetch frequent_item_sets, got: \n${JSON.stringify(body, null, 2)}`);
+ emitError(`Failed to fetch frequent_item_sets.`);
return {
fields: [],
df: [],
@@ -162,17 +162,17 @@ export async function fetchFrequentItems(
const totalDocCountFi = (body.hits.total as estypes.SearchTotalHits).value;
- const frequentItems = isRandomSamplerAggregation(body.aggregations)
+ const frequentItemSets = isRandomSamplerAggregation(body.aggregations)
? body.aggregations.sample.fi
: body.aggregations.fi;
- const shape = frequentItems.buckets.length;
+ const shape = frequentItemSets.buckets.length;
let maximum = shape;
if (maximum > 50000) {
maximum = 50000;
}
- const fiss = frequentItems.buckets;
+ const fiss = frequentItemSets.buckets;
fiss.length = maximum;
const results: ItemsetResult[] = [];
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_change_point_groups.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_change_point_groups.test.ts
index 2496e9e927f0e..7340d8e834d10 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_change_point_groups.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_change_point_groups.test.ts
@@ -6,7 +6,7 @@
*/
import { fields } from '../../../common/__mocks__/artificial_logs/fields';
-import { frequentItems } from '../../../common/__mocks__/artificial_logs/frequent_items';
+import { frequentItemSets } from '../../../common/__mocks__/artificial_logs/frequent_item_sets';
import { changePoints } from '../../../common/__mocks__/artificial_logs/change_points';
import { finalChangePointGroups } from '../../../common/__mocks__/artificial_logs/final_change_point_groups';
@@ -14,7 +14,7 @@ import { getChangePointGroups } from './get_change_point_groups';
describe('getChangePointGroups', () => {
it('gets change point groups', () => {
- const changePointGroups = getChangePointGroups(frequentItems, changePoints, fields);
+ const changePointGroups = getChangePointGroups(frequentItemSets, changePoints, fields);
expect(changePointGroups).toEqual(finalChangePointGroups);
});
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_change_point_groups.ts b/x-pack/plugins/aiops/server/routes/queries/get_change_point_groups.ts
index fa0722b15c9a4..5b9920c180d45 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_change_point_groups.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_change_point_groups.ts
@@ -8,12 +8,12 @@
import type { ChangePoint, ChangePointGroup } from '@kbn/ml-agg-utils';
import { duplicateIdentifier } from './duplicate_identifier';
-import { dropDuplicates, groupDuplicates } from './fetch_frequent_items';
+import { dropDuplicates, groupDuplicates } from './fetch_frequent_item_sets';
import { getFieldValuePairCounts } from './get_field_value_pair_counts';
import { getMarkedDuplicates } from './get_marked_duplicates';
import { getSimpleHierarchicalTree } from './get_simple_hierarchical_tree';
import { getSimpleHierarchicalTreeLeaves } from './get_simple_hierarchical_tree_leaves';
-import { getFilteredFrequentItems } from './get_filtered_frequent_items';
+import { getFilteredFrequentItemSets } from './get_filtered_frequent_item_sets';
import { getGroupsWithReaddedDuplicates } from './get_groups_with_readded_duplicates';
import { getMissingChangePoints } from './get_missing_change_points';
import { transformChangePointToGroup } from './transform_change_point_to_group';
@@ -24,22 +24,22 @@ export function getChangePointGroups(
changePoints: ChangePoint[],
fields: string[]
): ChangePointGroup[] {
- // These are the deduplicated change points we pass to the `frequent_items` aggregation.
+ // These are the deduplicated change points we pass to the `frequent_item_sets` aggregation.
const deduplicatedChangePoints = dropDuplicates(changePoints, duplicateIdentifier);
// We use the grouped change points to later repopulate
- // the `frequent_items` result with the missing duplicates.
+ // the `frequent_item_sets` result with the missing duplicates.
const groupedChangePoints = groupDuplicates(changePoints, duplicateIdentifier).filter(
(g) => g.group.length > 1
);
- const filteredDf = getFilteredFrequentItems(itemsets, changePoints);
+ const filteredDf = getFilteredFrequentItemSets(itemsets, changePoints);
- // `frequent_items` returns lot of different small groups of field/value pairs that co-occur.
+ // `frequent_item_sets` returns lot of different small groups of field/value pairs that co-occur.
// The following steps analyse these small groups, identify overlap between these groups,
// and then summarize them in larger groups where possible.
- // Get a tree structure based on `frequent_items`.
+ // Get a tree structure based on `frequent_item_sets`.
const { root } = getSimpleHierarchicalTree(filteredDf, true, false, fields);
// Each leave of the tree will be a summarized group of co-occuring field/value pairs.
@@ -48,7 +48,7 @@ export function getChangePointGroups(
// To be able to display a more cleaned up results table in the UI, we identify field/value pairs
// that occur in multiple groups. This will allow us to highlight field/value pairs that are
// unique to a group in a better way. This step will also re-add duplicates we identified in the
- // beginning and didn't pass on to the `frequent_items` agg.
+ // beginning and didn't pass on to the `frequent_item_sets` agg.
const fieldValuePairCounts = getFieldValuePairCounts(treeLeaves);
const changePointGroupsWithMarkedDuplicates = getMarkedDuplicates(
treeLeaves,
@@ -59,7 +59,7 @@ export function getChangePointGroups(
groupedChangePoints
);
- // Some field/value pairs might not be part of the `frequent_items` result set, for example
+ // Some field/value pairs might not be part of the `frequent_item_sets` result set, for example
// because they don't co-occur with other field/value pairs or because of the limits we set on the query.
// In this next part we identify those missing pairs and add them as individual groups.
const missingChangePoints = getMissingChangePoints(deduplicatedChangePoints, changePointGroups);
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_field_value_pair_counts.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_field_value_pair_counts.test.ts
index d5b56751fa173..7c104e2ecb586 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_field_value_pair_counts.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_field_value_pair_counts.test.ts
@@ -7,7 +7,7 @@
import { changePointGroups } from '../../../common/__mocks__/farequote/change_point_groups';
import { fields } from '../../../common/__mocks__/artificial_logs/fields';
-import { filteredFrequentItems } from '../../../common/__mocks__/artificial_logs/filtered_frequent_items';
+import { filteredFrequentItemSets } from '../../../common/__mocks__/artificial_logs/filtered_frequent_item_sets';
import { getFieldValuePairCounts } from './get_field_value_pair_counts';
import { getSimpleHierarchicalTree } from './get_simple_hierarchical_tree';
@@ -30,7 +30,7 @@ describe('getFieldValuePairCounts', () => {
it('returns a nested record with field/value pair counts for artificial logs', () => {
const simpleHierarchicalTree = getSimpleHierarchicalTree(
- filteredFrequentItems,
+ filteredFrequentItemSets,
true,
false,
fields
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_item_sets.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_item_sets.test.ts
new file mode 100644
index 0000000000000..ed907770d3484
--- /dev/null
+++ b/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_item_sets.test.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { changePoints } from '../../../common/__mocks__/artificial_logs/change_points';
+import { frequentItemSets } from '../../../common/__mocks__/artificial_logs/frequent_item_sets';
+import { filteredFrequentItemSets } from '../../../common/__mocks__/artificial_logs/filtered_frequent_item_sets';
+
+import { getFilteredFrequentItemSets } from './get_filtered_frequent_item_sets';
+
+describe('getFilteredFrequentItemSets', () => {
+ it('filter frequent item set based on provided change points', () => {
+ expect(getFilteredFrequentItemSets(frequentItemSets, changePoints)).toStrictEqual(
+ filteredFrequentItemSets
+ );
+ });
+});
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_items.ts b/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_item_sets.ts
similarity index 91%
rename from x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_items.ts
rename to x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_item_sets.ts
index e071621f6d9f7..15ba268fa733c 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_items.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_item_sets.ts
@@ -11,10 +11,10 @@ import type { ChangePoint } from '@kbn/ml-agg-utils';
import type { ItemsetResult } from '../../../common/types';
-// The way the `frequent_items` aggregation works could return item sets that include
+// The way the `frequent_item_sets` aggregation works could return item sets that include
// field/value pairs that are not part of the original list of significant change points.
// This cleans up groups and removes those unrelated field/value pairs.
-export function getFilteredFrequentItems(
+export function getFilteredFrequentItemSets(
itemsets: ItemsetResult[],
changePoints: ChangePoint[]
): ItemsetResult[] {
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_items.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_items.test.ts
deleted file mode 100644
index 8399c0366dea1..0000000000000
--- a/x-pack/plugins/aiops/server/routes/queries/get_filtered_frequent_items.test.ts
+++ /dev/null
@@ -1,20 +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 { changePoints } from '../../../common/__mocks__/artificial_logs/change_points';
-import { frequentItems } from '../../../common/__mocks__/artificial_logs/frequent_items';
-import { filteredFrequentItems } from '../../../common/__mocks__/artificial_logs/filtered_frequent_items';
-
-import { getFilteredFrequentItems } from './get_filtered_frequent_items';
-
-describe('getFilteredFrequentItems', () => {
- it('filter frequent item set based on provided change points', () => {
- expect(getFilteredFrequentItems(frequentItems, changePoints)).toStrictEqual(
- filteredFrequentItems
- );
- });
-});
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_groups_with_readded_duplicates.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_groups_with_readded_duplicates.test.ts
index 50b5719e49fa4..257acff9793b5 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_groups_with_readded_duplicates.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_groups_with_readded_duplicates.test.ts
@@ -10,7 +10,7 @@ import { changePoints } from '../../../common/__mocks__/artificial_logs/change_p
import { duplicateIdentifier } from './duplicate_identifier';
import { getGroupsWithReaddedDuplicates } from './get_groups_with_readded_duplicates';
-import { groupDuplicates } from './fetch_frequent_items';
+import { groupDuplicates } from './fetch_frequent_item_sets';
import { getFieldValuePairCounts } from './get_field_value_pair_counts';
import { getMarkedDuplicates } from './get_marked_duplicates';
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_marked_duplicates.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_marked_duplicates.test.ts
index e44a26d70494c..f84f08e235b6a 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_marked_duplicates.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_marked_duplicates.test.ts
@@ -7,7 +7,7 @@
import { changePointGroups } from '../../../common/__mocks__/farequote/change_point_groups';
import { fields } from '../../../common/__mocks__/artificial_logs/fields';
-import { filteredFrequentItems } from '../../../common/__mocks__/artificial_logs/filtered_frequent_items';
+import { filteredFrequentItemSets } from '../../../common/__mocks__/artificial_logs/filtered_frequent_item_sets';
import { getFieldValuePairCounts } from './get_field_value_pair_counts';
import { getMarkedDuplicates } from './get_marked_duplicates';
@@ -59,7 +59,7 @@ describe('markDuplicates', () => {
it('marks duplicates based on change point groups for artificial logs', () => {
const simpleHierarchicalTree = getSimpleHierarchicalTree(
- filteredFrequentItems,
+ filteredFrequentItemSets,
true,
false,
fields
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_missing_change_points.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_missing_change_points.test.ts
index 477321b74e007..757097c84e0c8 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_missing_change_points.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_missing_change_points.test.ts
@@ -10,7 +10,7 @@ import { changePoints } from '../../../common/__mocks__/artificial_logs/change_p
import { duplicateIdentifier } from './duplicate_identifier';
import { getGroupsWithReaddedDuplicates } from './get_groups_with_readded_duplicates';
-import { dropDuplicates, groupDuplicates } from './fetch_frequent_items';
+import { dropDuplicates, groupDuplicates } from './fetch_frequent_item_sets';
import { getFieldValuePairCounts } from './get_field_value_pair_counts';
import { getMarkedDuplicates } from './get_marked_duplicates';
import { getMissingChangePoints } from './get_missing_change_points';
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_simple_hierarchical_tree.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_simple_hierarchical_tree.test.ts
index 36cc113ad7be0..e7f40a795bc16 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_simple_hierarchical_tree.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_simple_hierarchical_tree.test.ts
@@ -6,7 +6,7 @@
*/
import { fields } from '../../../common/__mocks__/artificial_logs/fields';
-import { filteredFrequentItems } from '../../../common/__mocks__/artificial_logs/filtered_frequent_items';
+import { filteredFrequentItemSets } from '../../../common/__mocks__/artificial_logs/filtered_frequent_item_sets';
import { getSimpleHierarchicalTree } from './get_simple_hierarchical_tree';
@@ -16,7 +16,7 @@ describe('getSimpleHierarchicalTree', () => {
// and make it comparable against a static representation.
expect(
JSON.parse(
- JSON.stringify(getSimpleHierarchicalTree(filteredFrequentItems, true, false, fields))
+ JSON.stringify(getSimpleHierarchicalTree(filteredFrequentItemSets, true, false, fields))
)
).toEqual({
root: {
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_simple_hierarchical_tree_leaves.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_simple_hierarchical_tree_leaves.test.ts
index 9567d38f3d402..89d345bf1d82d 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_simple_hierarchical_tree_leaves.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_simple_hierarchical_tree_leaves.test.ts
@@ -6,7 +6,7 @@
*/
import { fields } from '../../../common/__mocks__/artificial_logs/fields';
-import { filteredFrequentItems } from '../../../common/__mocks__/artificial_logs/filtered_frequent_items';
+import { filteredFrequentItemSets } from '../../../common/__mocks__/artificial_logs/filtered_frequent_item_sets';
import { getSimpleHierarchicalTree } from './get_simple_hierarchical_tree';
import { getSimpleHierarchicalTreeLeaves } from './get_simple_hierarchical_tree_leaves';
@@ -14,7 +14,7 @@ import { getSimpleHierarchicalTreeLeaves } from './get_simple_hierarchical_tree_
describe('getSimpleHierarchicalTreeLeaves', () => {
it('returns the hierarchical tree leaves', () => {
const simpleHierarchicalTree = getSimpleHierarchicalTree(
- filteredFrequentItems,
+ filteredFrequentItemSets,
true,
false,
fields
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_value_counts.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_value_counts.test.ts
index 744179c485caa..89d7880b5250b 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_value_counts.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_value_counts.test.ts
@@ -5,12 +5,12 @@
* 2.0.
*/
-import { frequentItems } from '../../../common/__mocks__/artificial_logs/frequent_items';
+import { frequentItemSets } from '../../../common/__mocks__/artificial_logs/frequent_item_sets';
import { getValueCounts } from './get_value_counts';
describe('getValueCounts', () => {
it('get value counts for field response_code', () => {
- expect(getValueCounts(frequentItems, 'response_code')).toEqual({
+ expect(getValueCounts(frequentItemSets, 'response_code')).toEqual({
'200': 1,
'404': 1,
'500': 3,
@@ -18,11 +18,11 @@ describe('getValueCounts', () => {
});
it('get value counts for field url', () => {
- expect(getValueCounts(frequentItems, 'url')).toEqual({ 'home.php': 6 });
+ expect(getValueCounts(frequentItemSets, 'url')).toEqual({ 'home.php': 6 });
});
it('get value counts for field user', () => {
- expect(getValueCounts(frequentItems, 'user')).toEqual({
+ expect(getValueCounts(frequentItemSets, 'user')).toEqual({
Mary: 1,
Paul: 1,
Peter: 3,
diff --git a/x-pack/plugins/aiops/server/routes/queries/get_values_descending.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_values_descending.test.ts
index cd4935b4fcc8f..e3519995fc916 100644
--- a/x-pack/plugins/aiops/server/routes/queries/get_values_descending.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/get_values_descending.test.ts
@@ -5,19 +5,19 @@
* 2.0.
*/
-import { frequentItems } from '../../../common/__mocks__/artificial_logs/frequent_items';
+import { frequentItemSets } from '../../../common/__mocks__/artificial_logs/frequent_item_sets';
import { getValuesDescending } from './get_values_descending';
describe('getValuesDescending', () => {
it('get descending values for field response_code', () => {
- expect(getValuesDescending(frequentItems, 'response_code')).toEqual(['500', '200', '404']);
+ expect(getValuesDescending(frequentItemSets, 'response_code')).toEqual(['500', '200', '404']);
});
it('get descending values for field url', () => {
- expect(getValuesDescending(frequentItems, 'url')).toEqual(['home.php']);
+ expect(getValuesDescending(frequentItemSets, 'url')).toEqual(['home.php']);
});
it('get descending values for field user', () => {
- expect(getValuesDescending(frequentItems, 'user')).toEqual(['Peter', 'Mary', 'Paul']);
+ expect(getValuesDescending(frequentItemSets, 'user')).toEqual(['Peter', 'Mary', 'Paul']);
});
});
diff --git a/x-pack/plugins/aiops/server/routes/queries/transform_change_point_to_group.test.ts b/x-pack/plugins/aiops/server/routes/queries/transform_change_point_to_group.test.ts
index 448f3003fc924..c1ed0bcbcb6ab 100644
--- a/x-pack/plugins/aiops/server/routes/queries/transform_change_point_to_group.test.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/transform_change_point_to_group.test.ts
@@ -10,7 +10,7 @@ import { changePoints } from '../../../common/__mocks__/artificial_logs/change_p
import { duplicateIdentifier } from './duplicate_identifier';
import { getGroupsWithReaddedDuplicates } from './get_groups_with_readded_duplicates';
-import { dropDuplicates, groupDuplicates } from './fetch_frequent_items';
+import { dropDuplicates, groupDuplicates } from './fetch_frequent_item_sets';
import { getFieldValuePairCounts } from './get_field_value_pair_counts';
import { getMarkedDuplicates } from './get_marked_duplicates';
import { getMissingChangePoints } from './get_missing_change_points';
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
index 3c97080e2d655..ea6145b8f6e99 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
+++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
@@ -17,7 +17,10 @@ import { parseDuration } from '../../lib';
export async function validateActions(
context: RulesClientContext,
alertType: UntypedNormalizedRuleType,
- data: Pick & { actions: NormalizedAlertAction[] }
+ data: Pick & {
+ actions: NormalizedAlertAction[];
+ },
+ allowMissingConnectorSecrets?: boolean
): Promise {
const { actions, notifyWhen, throttle } = data;
const hasRuleLevelNotifyWhen = typeof notifyWhen !== 'undefined';
@@ -35,20 +38,26 @@ export async function validateActions(
const actionsUsingConnectorsWithMissingSecrets = actionResults.filter(
(result) => result.isMissingSecrets
);
-
if (actionsUsingConnectorsWithMissingSecrets.length) {
- errors.push(
- i18n.translate('xpack.alerting.rulesClient.validateActions.misconfiguredConnector', {
- defaultMessage: 'Invalid connectors: {groups}',
- values: {
- groups: actionsUsingConnectorsWithMissingSecrets
- .map((connector) => connector.name)
- .join(', '),
- },
- })
- );
+ if (allowMissingConnectorSecrets) {
+ context.logger.error(
+ `Invalid connectors with "allowMissingConnectorSecrets": ${actionsUsingConnectorsWithMissingSecrets
+ .map((connector) => connector.name)
+ .join(', ')}`
+ );
+ } else {
+ errors.push(
+ i18n.translate('xpack.alerting.rulesClient.validateActions.misconfiguredConnector', {
+ defaultMessage: 'Invalid connectors: {groups}',
+ values: {
+ groups: actionsUsingConnectorsWithMissingSecrets
+ .map((connector) => connector.name)
+ .join(', '),
+ },
+ })
+ );
+ }
}
-
// check for actions with invalid action groups
const { actionGroups: alertTypeActionGroups } = alertType;
const usedAlertActionGroups = actions.map((action) => action.group);
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts
index e8dcefe4a9ef1..09dc8f62494b8 100644
--- a/x-pack/plugins/alerting/server/rules_client/methods/create.ts
+++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts
@@ -46,11 +46,12 @@ export interface CreateOptions {
| 'nextRun'
> & { actions: NormalizedAlertAction[] };
options?: SavedObjectOptions;
+ allowMissingConnectorSecrets?: boolean;
}
export async function create(
context: RulesClientContext,
- { data, options }: CreateOptions
+ { data, options, allowMissingConnectorSecrets }: CreateOptions
): Promise> {
const id = options?.id || SavedObjectsUtils.generateId();
@@ -104,10 +105,11 @@ export async function create(
}
}
- await validateActions(context, ruleType, data);
+ await validateActions(context, ruleType, data, allowMissingConnectorSecrets);
await withSpan({ name: 'validateActions', type: 'rules' }, () =>
- validateActions(context, ruleType, data)
+ validateActions(context, ruleType, data, allowMissingConnectorSecrets)
);
+
// Throw error if schedule interval is less than the minimum and we are enforcing it
const intervalInMs = parseDuration(data.schedule.interval);
if (
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts
index 8fa3855b95707..5e116f9f21b28 100644
--- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts
+++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts
@@ -38,22 +38,23 @@ export interface UpdateOptions {
throttle?: string | null;
notifyWhen?: RuleNotifyWhenType | null;
};
+ allowMissingConnectorSecrets?: boolean;
}
export async function update(
context: RulesClientContext,
- { id, data }: UpdateOptions
+ { id, data, allowMissingConnectorSecrets }: UpdateOptions
): Promise> {
return await retryIfConflicts(
context.logger,
`rulesClient.update('${id}')`,
- async () => await updateWithOCC(context, { id, data })
+ async () => await updateWithOCC(context, { id, data, allowMissingConnectorSecrets })
);
}
async function updateWithOCC(
context: RulesClientContext,
- { id, data }: UpdateOptions
+ { id, data, allowMissingConnectorSecrets }: UpdateOptions
): Promise> {
let alertSavedObject: SavedObject;
@@ -99,7 +100,11 @@ async function updateWithOCC(
context.ruleTypeRegistry.ensureRuleTypeEnabled(alertSavedObject.attributes.alertTypeId);
- const updateResult = await updateAlert(context, { id, data }, alertSavedObject);
+ const updateResult = await updateAlert(
+ context,
+ { id, data, allowMissingConnectorSecrets },
+ alertSavedObject
+ );
await Promise.all([
alertSavedObject.attributes.apiKey
@@ -138,7 +143,7 @@ async function updateWithOCC(
async function updateAlert(
context: RulesClientContext,
- { id, data }: UpdateOptions,
+ { id, data, allowMissingConnectorSecrets }: UpdateOptions,
{ attributes, version }: SavedObject
): Promise> {
const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId);
@@ -156,7 +161,7 @@ async function updateAlert(
// Validate
const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params);
- await validateActions(context, ruleType, data);
+ await validateActions(context, ruleType, data, allowMissingConnectorSecrets);
// Throw error if schedule interval is less than the minimum and we are enforcing it
const intervalInMs = parseDuration(data.schedule.interval);
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
index c11dc8be21ca4..dddfbe85f9ee3 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
@@ -3047,4 +3047,120 @@ describe('create()', () => {
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
});
+ test('should create a rule even if action is missing secret when allowMissingConnectorSecrets is true', async () => {
+ const data = getMockData({
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ params: {
+ foo: true,
+ },
+ },
+ {
+ group: 'default',
+ id: '1',
+ params: {
+ foo: true,
+ },
+ },
+ {
+ group: 'default',
+ id: '2',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ });
+ actionsClient.getBulk.mockReset();
+ actionsClient.getBulk.mockResolvedValue([
+ {
+ id: '1',
+ actionTypeId: '.slack',
+ config: {},
+ isMissingSecrets: true,
+ name: 'Slack connector',
+ isPreconfigured: false,
+ isDeprecated: false,
+ },
+ ]);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: '123',
+ schedule: { interval: '1m' },
+ params: {
+ bar: true,
+ },
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ notifyWhen: null,
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ actionTypeId: '.slack',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ {
+ name: 'action_1',
+ type: 'action',
+ id: '1',
+ },
+ {
+ name: 'action_2',
+ type: 'action',
+ id: '2',
+ },
+ ],
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ scheduledTaskId: 'task-123',
+ },
+ references: [],
+ });
+ const result = await rulesClient.create({ data, allowMissingConnectorSecrets: true });
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "actions": Array [
+ Object {
+ "actionTypeId": ".slack",
+ "group": "default",
+ "id": "1",
+ "params": Object {
+ "foo": true,
+ },
+ },
+ ],
+ "alertTypeId": "123",
+ "createdAt": 2019-02-12T21:01:22.479Z,
+ "id": "1",
+ "notifyWhen": null,
+ "params": Object {
+ "bar": true,
+ },
+ "schedule": Object {
+ "interval": "1m",
+ },
+ "scheduledTaskId": "task-123",
+ "updatedAt": 2019-02-12T21:01:22.479Z,
+ }
+ `);
+ });
});
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
index 44c4eeb50fe27..4d16f3e5d66a0 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
@@ -2022,6 +2022,155 @@ describe('update()', () => {
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
});
+ test('should update a rule even if action is missing secret when allowMissingConnectorSecrets is true', async () => {
+ // Reset from default behaviour
+ actionsClient.getBulk.mockReset();
+ actionsClient.getBulk.mockResolvedValue([
+ {
+ id: '1',
+ actionTypeId: '.slack',
+ config: {},
+ isMissingSecrets: true,
+ name: 'slack connector',
+ isPreconfigured: false,
+ isDeprecated: false,
+ },
+ ]);
+ actionsClient.isPreconfigured.mockReset();
+ actionsClient.isPreconfigured.mockReturnValueOnce(false);
+ actionsClient.isPreconfigured.mockReturnValueOnce(true);
+ actionsClient.isPreconfigured.mockReturnValueOnce(true);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ enabled: true,
+ schedule: { interval: '1m' },
+ params: {
+ bar: true,
+ },
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ actionTypeId: '.slack',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ notifyWhen: 'onActiveAlert',
+ scheduledTaskId: 'task-123',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ {
+ name: 'param:soRef_0',
+ type: 'someSavedObjectType',
+ id: '9',
+ },
+ ],
+ });
+ const result = await rulesClient.update({
+ id: '1',
+ data: {
+ schedule: { interval: '1m' },
+ name: 'abc',
+ tags: ['foo'],
+ params: {
+ bar: true,
+ },
+ throttle: null,
+ notifyWhen: 'onActiveAlert',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ },
+ allowMissingConnectorSecrets: true,
+ });
+
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenNthCalledWith(
+ 1,
+ 'alert',
+ {
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ actionTypeId: '.slack',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ alertTypeId: 'myType',
+ apiKey: null,
+ apiKeyOwner: null,
+ consumer: 'myApp',
+ enabled: true,
+ meta: { versionApiKeyLastmodified: 'v7.10.0' },
+ name: 'abc',
+ notifyWhen: 'onActiveAlert',
+ params: { bar: true },
+ schedule: { interval: '1m' },
+ scheduledTaskId: 'task-123',
+ tags: ['foo'],
+ throttle: null,
+ updatedAt: '2019-02-12T21:01:22.479Z',
+ updatedBy: 'elastic',
+ },
+ {
+ id: '1',
+ overwrite: true,
+ references: [{ id: '1', name: 'action_0', type: 'action' }],
+ version: '123',
+ }
+ );
+
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "actions": Array [
+ Object {
+ "actionTypeId": ".slack",
+ "group": "default",
+ "id": "1",
+ "params": Object {
+ "foo": true,
+ },
+ },
+ ],
+ "createdAt": 2019-02-12T21:01:22.479Z,
+ "enabled": true,
+ "id": "1",
+ "notifyWhen": "onActiveAlert",
+ "params": Object {
+ "bar": true,
+ },
+ "schedule": Object {
+ "interval": "1m",
+ },
+ "scheduledTaskId": "task-123",
+ "updatedAt": 2019-02-12T21:01:22.479Z,
+ }
+ `);
+ expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
+ namespace: 'default',
+ });
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
+ expect(actionsClient.isPreconfigured).toHaveBeenCalledTimes(1);
+ });
test('logs warning when creating with an interval less than the minimum configured one when enforce = false', async () => {
actionsClient.getBulk.mockReset();
diff --git a/x-pack/plugins/apm/common/es_fields/__snapshots__/es_fields.test.ts.snap b/x-pack/plugins/apm/common/es_fields/__snapshots__/es_fields.test.ts.snap
index 815665f5f0b43..0cb355aaab572 100644
--- a/x-pack/plugins/apm/common/es_fields/__snapshots__/es_fields.test.ts.snap
+++ b/x-pack/plugins/apm/common/es_fields/__snapshots__/es_fields.test.ts.snap
@@ -21,6 +21,8 @@ exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Error CLIENT_GEO_COUNTRY_NAME 1`] = `undefined`;
+exports[`Error CLIENT_GEO_REGION_ISO_CODE 1`] = `undefined`;
+
exports[`Error CLIENT_GEO_REGION_NAME 1`] = `undefined`;
exports[`Error CLOUD 1`] = `
@@ -325,6 +327,8 @@ exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Span CLIENT_GEO_COUNTRY_NAME 1`] = `undefined`;
+exports[`Span CLIENT_GEO_REGION_ISO_CODE 1`] = `undefined`;
+
exports[`Span CLIENT_GEO_REGION_NAME 1`] = `undefined`;
exports[`Span CLOUD 1`] = `
@@ -612,6 +616,8 @@ exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Transaction CLIENT_GEO_COUNTRY_NAME 1`] = `undefined`;
+exports[`Transaction CLIENT_GEO_REGION_ISO_CODE 1`] = `undefined`;
+
exports[`Transaction CLIENT_GEO_REGION_NAME 1`] = `undefined`;
exports[`Transaction CLOUD 1`] = `
diff --git a/x-pack/plugins/apm/common/es_fields/apm.ts b/x-pack/plugins/apm/common/es_fields/apm.ts
index 6f5420af51c7a..e38d805565c48 100644
--- a/x-pack/plugins/apm/common/es_fields/apm.ts
+++ b/x-pack/plugins/apm/common/es_fields/apm.ts
@@ -171,6 +171,7 @@ export const EVENT_NAME = 'event.name';
// Location
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
+export const CLIENT_GEO_REGION_ISO_CODE = 'client.geo.region_iso_code';
export const CLIENT_GEO_COUNTRY_NAME = 'client.geo.country_name';
export const CLIENT_GEO_CITY_NAME = 'client.geo.city_name';
export const CLIENT_GEO_REGION_NAME = 'client.geo.region_name';
diff --git a/x-pack/plugins/apm/common/mobile/constants.ts b/x-pack/plugins/apm/common/mobile/constants.ts
new file mode 100644
index 0000000000000..d2b8fdc809156
--- /dev/null
+++ b/x-pack/plugins/apm/common/mobile/constants.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export enum MapTypes {
+ Http = 'http_requests',
+ Session = 'unique_sessions',
+}
+
+export enum MobileSpanSubtype {
+ Http = 'http',
+}
+
+export enum MobileSpanType {
+ External = 'external',
+}
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/embedded_map.test.tsx b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/embedded_map.test.tsx
similarity index 91%
rename from x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/embedded_map.test.tsx
rename to x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/embedded_map.test.tsx
index 78afdb121df48..88e94ef82a61c 100644
--- a/x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/embedded_map.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/embedded_map.test.tsx
@@ -12,12 +12,14 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context';
import { MemoryRouter } from 'react-router-dom';
+import { MapTypes } from '../../../../../../common/mobile/constants';
describe('Embedded Map', () => {
it('it renders', async () => {
const mockSetLayerList = jest.fn();
const mockUpdateInput = jest.fn();
const mockRender = jest.fn();
+ const mockReload = jest.fn();
const mockEmbeddable = embeddablePluginMock.createStartContract();
mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({
@@ -25,6 +27,7 @@ describe('Embedded Map', () => {
setLayerList: mockSetLayerList,
updateInput: mockUpdateInput,
render: mockRender,
+ reload: mockReload,
}),
}));
@@ -37,6 +40,7 @@ describe('Embedded Map', () => {
{
+ const setLayerList = async () => {
+ if (embeddable && !isErrorEmbeddable(embeddable)) {
+ const layerList = await getLayerList({ selectedMap, maps });
+ await Promise.all([
+ embeddable.setLayerList(layerList),
+ embeddable.reload(),
+ ]);
+ }
+ };
+
+ setLayerList();
+ }, [embeddable, selectedMap, maps]);
+
useEffect(() => {
if (embeddable) {
embeddable.updateInput({
@@ -140,7 +153,7 @@ function EmbeddedMapComponent({
},
});
}
- }, [start, end, kuery, filters, embeddable]);
+ }, [start, end, kuery, filters, embeddable, selectedMap]);
return (
<>
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/index.tsx b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/index.tsx
new file mode 100644
index 0000000000000..bf8331b69555b
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/index.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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 } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import type { Filter } from '@kbn/es-query';
+import { EuiSuperSelect, EuiText } from '@elastic/eui';
+import { EmbeddedMap } from './embedded_map';
+import { MapTypes } from '../../../../../../common/mobile/constants';
+
+const availableMaps: Array<{
+ id: MapTypes;
+ label: string;
+ description: string;
+}> = [
+ {
+ id: MapTypes.Http,
+ label: i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.dropdown.http.requests',
+ {
+ defaultMessage: 'HTTP requests',
+ }
+ ),
+ description: i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.dropdown.http.requests.subtitle',
+ {
+ defaultMessage:
+ 'HTTP defines a set of request methods to indicate the desired action to be performed for a given resource',
+ }
+ ),
+ },
+ {
+ id: MapTypes.Session,
+ label: i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.dropdown.sessions',
+ {
+ defaultMessage: 'Sessions',
+ }
+ ),
+ description: i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.dropdown.sessions.subtitle',
+ {
+ defaultMessage:
+ 'An application session begins when a user starts an application and ends when the application exits.',
+ }
+ ),
+ },
+];
+
+export function GeoMap({
+ start,
+ end,
+ kuery,
+ filters,
+}: {
+ start: string;
+ end: string;
+ kuery?: string;
+ filters: Filter[];
+}) {
+ const [selectedMap, setMap] = useState(MapTypes.Http);
+
+ const currentMap =
+ availableMaps.find(({ id }) => id === selectedMap) ?? availableMaps[0];
+
+ return (
+ <>
+
+
+
+
+ {i18n.translate('xpack.apm.serviceOverview.embeddedMap.title', {
+ defaultMessage: 'Geographic regions',
+ })}
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.subtitle',
+ {
+ defaultMessage:
+ 'Map showing the total number of {currentMap} based on country and region',
+ values: { currentMap: currentMap.label },
+ }
+ )}
+
+
+
+
+ ({
+ inputDisplay: item.label,
+ value: item.id,
+ dropdownDisplay: (
+ <>
+ {item.label}
+
+ {item.description}
+
+ >
+ ),
+ }))}
+ valueOfSelected={selectedMap}
+ onChange={(value: MapTypes) => setMap(value)}
+ itemLayoutAlign="top"
+ hasDividers
+ />
+
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_http_requests_map_layer_list.ts b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_http_requests_map_layer_list.ts
new file mode 100644
index 0000000000000..8954626546290
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_http_requests_map_layer_list.ts
@@ -0,0 +1,149 @@
+/*
+ * 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 {
+ EMSFileSourceDescriptor,
+ LayerDescriptor as BaseLayerDescriptor,
+ VectorLayerDescriptor as BaseVectorLayerDescriptor,
+ AGG_TYPE,
+ LAYER_TYPE,
+ SOURCE_TYPES,
+} from '@kbn/maps-plugin/common';
+import { ProcessorEvent } from '@kbn/observability-plugin/common';
+import { v4 as uuidv4 } from 'uuid';
+import type { MapsStartApi } from '@kbn/maps-plugin/public';
+import { i18n } from '@kbn/i18n';
+import {
+ CLIENT_GEO_COUNTRY_ISO_CODE,
+ CLIENT_GEO_REGION_ISO_CODE,
+ PROCESSOR_EVENT,
+ SPAN_SUBTYPE,
+ SPAN_TYPE,
+} from '../../../../../../../common/es_fields/apm';
+import { APM_STATIC_DATA_VIEW_ID } from '../../../../../../../common/data_view_constants';
+import { getLayerStyle, PalleteColors } from './get_map_layer_style';
+import {
+ MobileSpanSubtype,
+ MobileSpanType,
+} from '../../../../../../../common/mobile/constants';
+
+interface VectorLayerDescriptor extends BaseVectorLayerDescriptor {
+ sourceDescriptor: EMSFileSourceDescriptor;
+}
+
+const COUNTRY_NAME = 'name';
+const PER_COUNTRY_LAYER_ID = 'per_country';
+const PER_REGION_LAYER_ID = 'per_region';
+const HTTP_REQUEST_PER_COUNTRY = `__kbnjoin__count__${PER_COUNTRY_LAYER_ID}`;
+const HTTP_REQUESTS_PER_REGION = `__kbnjoin__count__${PER_REGION_LAYER_ID}`;
+
+const label = i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.httpRequests.metric.label',
+ {
+ defaultMessage: 'HTTP requests',
+ }
+);
+
+export async function getHttpRequestsLayerList(maps?: MapsStartApi) {
+ const whereQuery = {
+ language: 'kuery',
+ query: `${PROCESSOR_EVENT}:${ProcessorEvent.span} and ${SPAN_SUBTYPE}:${MobileSpanSubtype.Http} and ${SPAN_TYPE}:${MobileSpanType.External}`,
+ };
+
+ const basemapLayerDescriptor =
+ await maps?.createLayerDescriptors?.createBasemapLayerDescriptor();
+
+ const httpRequestsByCountryLayer: VectorLayerDescriptor = {
+ joins: [
+ {
+ leftField: 'iso2',
+ right: {
+ type: SOURCE_TYPES.ES_TERM_SOURCE,
+ id: PER_COUNTRY_LAYER_ID,
+ term: CLIENT_GEO_COUNTRY_ISO_CODE,
+ metrics: [
+ {
+ type: AGG_TYPE.COUNT,
+ label,
+ },
+ ],
+ whereQuery,
+ indexPatternId: APM_STATIC_DATA_VIEW_ID,
+ applyGlobalQuery: true,
+ applyGlobalTime: true,
+ applyForceRefresh: true,
+ },
+ },
+ ],
+ sourceDescriptor: {
+ type: SOURCE_TYPES.EMS_FILE,
+ id: 'world_countries',
+ tooltipProperties: [COUNTRY_NAME],
+ },
+ style: getLayerStyle(HTTP_REQUEST_PER_COUNTRY, PalleteColors.BluetoRed),
+ id: uuidv4(),
+ label: i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.httpRequests.country.label',
+ {
+ defaultMessage: 'HTTP requests per country',
+ }
+ ),
+ minZoom: 0,
+ maxZoom: 2,
+ alpha: 0.75,
+ visible: true,
+ type: LAYER_TYPE.GEOJSON_VECTOR,
+ };
+
+ const httpRequestsByRegionLayer: VectorLayerDescriptor = {
+ joins: [
+ {
+ leftField: 'region_iso_code',
+ right: {
+ type: SOURCE_TYPES.ES_TERM_SOURCE,
+ id: PER_REGION_LAYER_ID,
+ term: CLIENT_GEO_REGION_ISO_CODE,
+ metrics: [
+ {
+ type: AGG_TYPE.COUNT,
+ label,
+ },
+ ],
+ whereQuery,
+ indexPatternId: APM_STATIC_DATA_VIEW_ID,
+ applyGlobalQuery: true,
+ applyGlobalTime: true,
+ applyForceRefresh: true,
+ },
+ },
+ ],
+ sourceDescriptor: {
+ type: SOURCE_TYPES.EMS_FILE,
+ id: 'administrative_regions_lvl2',
+ tooltipProperties: ['region_iso_code'],
+ },
+ style: getLayerStyle(HTTP_REQUESTS_PER_REGION, PalleteColors.YellowtoRed),
+ id: uuidv4(),
+ label: i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.httpRequests.region.label',
+ {
+ defaultMessage: 'HTTP requests per region',
+ }
+ ),
+ minZoom: 1,
+ maxZoom: 24,
+ alpha: 0.75,
+ visible: true,
+ type: LAYER_TYPE.GEOJSON_VECTOR,
+ };
+
+ return [
+ ...(basemapLayerDescriptor ? [basemapLayerDescriptor] : []),
+ httpRequestsByRegionLayer,
+ httpRequestsByCountryLayer,
+ ] as BaseLayerDescriptor[];
+}
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_layer_list.ts b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_layer_list.ts
new file mode 100644
index 0000000000000..152f1874b143b
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_layer_list.ts
@@ -0,0 +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.
+ */
+import type { MapsStartApi } from '@kbn/maps-plugin/public';
+import { LayerDescriptor } from '@kbn/maps-plugin/common';
+import { getHttpRequestsLayerList } from './get_http_requests_map_layer_list';
+import { getSessionMapLayerList } from './get_session_map_layer_list';
+import { MapTypes } from '../../../../../../../common/mobile/constants';
+
+export async function getLayerList({
+ selectedMap,
+ maps,
+}: {
+ selectedMap: MapTypes;
+ maps?: MapsStartApi;
+}): Promise {
+ switch (selectedMap) {
+ case MapTypes.Http:
+ return await getHttpRequestsLayerList(maps);
+ case MapTypes.Session:
+ return await getSessionMapLayerList(maps);
+ default:
+ return await getHttpRequestsLayerList(maps);
+ }
+}
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_map_layer_style.ts b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_map_layer_style.ts
new file mode 100644
index 0000000000000..aed6fb2d39b7b
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_map_layer_style.ts
@@ -0,0 +1,90 @@
+/*
+ * 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 {
+ VectorStyleDescriptor,
+ COLOR_MAP_TYPE,
+ FIELD_ORIGIN,
+ LABEL_BORDER_SIZES,
+ LABEL_POSITIONS,
+ STYLE_TYPE,
+ SYMBOLIZE_AS_TYPES,
+} from '@kbn/maps-plugin/common';
+
+export enum PalleteColors {
+ BluetoRed = 'Blue to Red',
+ YellowtoRed = 'Yellow to Red',
+}
+
+export function getLayerStyle(
+ fieldName: string,
+ color: PalleteColors
+): VectorStyleDescriptor {
+ return {
+ type: 'VECTOR',
+ properties: {
+ icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } },
+ fillColor: {
+ type: STYLE_TYPE.DYNAMIC,
+ options: {
+ color,
+ colorCategory: 'palette_0',
+ fieldMetaOptions: { isEnabled: true, sigma: 3 },
+ type: COLOR_MAP_TYPE.ORDINAL,
+ field: {
+ name: fieldName,
+ origin: FIELD_ORIGIN.JOIN,
+ },
+ useCustomColorRamp: false,
+ },
+ },
+ lineColor: {
+ type: STYLE_TYPE.DYNAMIC,
+ options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } },
+ },
+ lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } },
+ iconSize: { type: STYLE_TYPE.STATIC, options: { size: 6 } },
+ iconOrientation: {
+ type: STYLE_TYPE.STATIC,
+ options: { orientation: 0 },
+ },
+ labelText: {
+ type: STYLE_TYPE.DYNAMIC,
+ options: {
+ field: {
+ name: fieldName,
+ origin: FIELD_ORIGIN.JOIN,
+ },
+ },
+ },
+ labelPosition: {
+ options: {
+ position: LABEL_POSITIONS.CENTER,
+ },
+ },
+ labelZoomRange: {
+ options: {
+ useLayerZoomRange: true,
+ minZoom: 0,
+ maxZoom: 24,
+ },
+ },
+ labelColor: {
+ type: STYLE_TYPE.STATIC,
+ options: { color: '#3d3d3d' },
+ },
+ labelSize: { type: STYLE_TYPE.STATIC, options: { size: 14 } },
+ labelBorderColor: {
+ type: STYLE_TYPE.STATIC,
+ options: { color: '#FFFFFF' },
+ },
+ symbolizeAs: { options: { value: SYMBOLIZE_AS_TYPES.CIRCLE } },
+ labelBorderSize: { options: { size: LABEL_BORDER_SIZES.SMALL } },
+ },
+ isTimeAware: true,
+ };
+}
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_session_map_layer_list.ts b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_session_map_layer_list.ts
new file mode 100644
index 0000000000000..a0d2b80b218fd
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/geo_map/map_layers/get_session_map_layer_list.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 {
+ EMSFileSourceDescriptor,
+ LayerDescriptor as BaseLayerDescriptor,
+ VectorLayerDescriptor as BaseVectorLayerDescriptor,
+ AGG_TYPE,
+ LAYER_TYPE,
+ SOURCE_TYPES,
+} from '@kbn/maps-plugin/common';
+import { v4 as uuidv4 } from 'uuid';
+import type { MapsStartApi } from '@kbn/maps-plugin/public';
+import { i18n } from '@kbn/i18n';
+import {
+ CLIENT_GEO_COUNTRY_ISO_CODE,
+ CLIENT_GEO_REGION_ISO_CODE,
+ SESSION_ID,
+} from '../../../../../../../common/es_fields/apm';
+import { APM_STATIC_DATA_VIEW_ID } from '../../../../../../../common/data_view_constants';
+import { getLayerStyle, PalleteColors } from './get_map_layer_style';
+
+interface VectorLayerDescriptor extends BaseVectorLayerDescriptor {
+ sourceDescriptor: EMSFileSourceDescriptor;
+}
+
+const PER_COUNTRY_LAYER_ID = 'per_country';
+const PER_REGION_LAYER_ID = 'per_region';
+const COUNTRY_NAME = 'name';
+const SESSION_PER_COUNTRY = `__kbnjoin__cardinality_of_session.id__${PER_COUNTRY_LAYER_ID}`;
+const SESSION_PER_REGION = `__kbnjoin__cardinality_of_session.id__${PER_REGION_LAYER_ID}`;
+
+const label = i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.session.metric.label',
+ {
+ defaultMessage: 'Sessions',
+ }
+);
+export async function getSessionMapLayerList(maps?: MapsStartApi) {
+ const basemapLayerDescriptor =
+ await maps?.createLayerDescriptors?.createBasemapLayerDescriptor();
+
+ const sessionsByCountryLayer: VectorLayerDescriptor = {
+ joins: [
+ {
+ leftField: 'iso2',
+ right: {
+ type: SOURCE_TYPES.ES_TERM_SOURCE,
+ id: PER_COUNTRY_LAYER_ID,
+ term: CLIENT_GEO_COUNTRY_ISO_CODE,
+ metrics: [
+ {
+ type: AGG_TYPE.UNIQUE_COUNT,
+ field: SESSION_ID,
+ label,
+ },
+ ],
+ indexPatternId: APM_STATIC_DATA_VIEW_ID,
+ applyGlobalQuery: true,
+ applyGlobalTime: true,
+ applyForceRefresh: true,
+ },
+ },
+ ],
+ sourceDescriptor: {
+ type: SOURCE_TYPES.EMS_FILE,
+ id: 'world_countries',
+ tooltipProperties: [COUNTRY_NAME],
+ },
+ style: getLayerStyle(SESSION_PER_COUNTRY, PalleteColors.BluetoRed),
+ id: uuidv4(),
+ label: i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.sessionCountry.metric.label',
+ {
+ defaultMessage: 'Sessions per country',
+ }
+ ),
+ minZoom: 0,
+ maxZoom: 2,
+ alpha: 0.75,
+ visible: true,
+ type: LAYER_TYPE.GEOJSON_VECTOR,
+ };
+
+ const sessionsByRegionLayer: VectorLayerDescriptor = {
+ joins: [
+ {
+ leftField: 'region_iso_code',
+ right: {
+ type: SOURCE_TYPES.ES_TERM_SOURCE,
+ id: PER_REGION_LAYER_ID,
+ term: CLIENT_GEO_REGION_ISO_CODE,
+ metrics: [
+ {
+ type: AGG_TYPE.UNIQUE_COUNT,
+ field: SESSION_ID,
+ label,
+ },
+ ],
+ indexPatternId: APM_STATIC_DATA_VIEW_ID,
+ applyGlobalQuery: true,
+ applyGlobalTime: true,
+ applyForceRefresh: true,
+ },
+ },
+ ],
+ sourceDescriptor: {
+ type: SOURCE_TYPES.EMS_FILE,
+ id: 'administrative_regions_lvl2',
+ tooltipProperties: ['region_iso_code'],
+ },
+ style: getLayerStyle(SESSION_PER_REGION, PalleteColors.YellowtoRed),
+ id: uuidv4(),
+ label: i18n.translate(
+ 'xpack.apm.serviceOverview.embeddedMap.sessionRegion.metric.label',
+ {
+ defaultMessage: 'Sessions per region',
+ }
+ ),
+ minZoom: 1,
+ maxZoom: 24,
+ alpha: 0.75,
+ visible: true,
+ type: LAYER_TYPE.GEOJSON_VECTOR,
+ };
+
+ return [
+ ...(basemapLayerDescriptor ? [basemapLayerDescriptor] : []),
+ sessionsByRegionLayer,
+ sessionsByCountryLayer,
+ ] as BaseLayerDescriptor[];
+}
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/mobile/service_overview/index.tsx
index 326a7ae22f085..6e0f786aaf731 100644
--- a/x-pack/plugins/apm/public/components/app/mobile/service_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/index.tsx
@@ -35,7 +35,7 @@ import {
SERVICE_VERSION,
} from '../../../../../common/es_fields/apm';
import { MostUsedChart } from './most_used_chart';
-import { LatencyMap } from './latency_map';
+import { GeoMap } from './geo_map';
import { FailedTransactionRateChart } from '../../../shared/charts/failed_transaction_rate_chart';
import { ServiceOverviewDependenciesTable } from '../../service_overview/service_overview_dependencies_table';
import { LatencyChart } from '../../../shared/charts/latency_chart';
@@ -65,7 +65,6 @@ export function MobileServiceOverview() {
osVersion,
appVersion,
netConnectionType,
- comparisonEnabled,
},
} = useApmParams('/mobile-services/{serviceName}/overview');
@@ -155,12 +154,11 @@ export function MobileServiceOverview() {
-
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/get_layer_list.ts b/x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/get_layer_list.ts
deleted file mode 100644
index d6f544cdd1d21..0000000000000
--- a/x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/get_layer_list.ts
+++ /dev/null
@@ -1,157 +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 {
- EMSFileSourceDescriptor,
- LayerDescriptor as BaseLayerDescriptor,
- VectorLayerDescriptor as BaseVectorLayerDescriptor,
- VectorStyleDescriptor,
- AGG_TYPE,
- COLOR_MAP_TYPE,
- FIELD_ORIGIN,
- LABEL_BORDER_SIZES,
- LABEL_POSITIONS,
- LAYER_TYPE,
- SOURCE_TYPES,
- STYLE_TYPE,
- SYMBOLIZE_AS_TYPES,
-} from '@kbn/maps-plugin/common';
-import { v4 as uuidv4 } from 'uuid';
-import type { MapsStartApi } from '@kbn/maps-plugin/public';
-import { i18n } from '@kbn/i18n';
-import {
- CLIENT_GEO_COUNTRY_ISO_CODE,
- TRANSACTION_DURATION,
-} from '../../../../../../common/es_fields/apm';
-import { APM_STATIC_DATA_VIEW_ID } from '../../../../../../common/data_view_constants';
-
-interface VectorLayerDescriptor extends BaseVectorLayerDescriptor {
- sourceDescriptor: EMSFileSourceDescriptor;
-}
-
-const FIELD_NAME = 'apm-service-overview-layer-country';
-const COUNTRY_NAME = 'name';
-const TRANSACTION_DURATION_COUNTRY = `__kbnjoin__avg_of_transaction.duration.us__${FIELD_NAME}`;
-
-function getLayerStyle(): VectorStyleDescriptor {
- return {
- type: 'VECTOR',
- properties: {
- icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } },
- fillColor: {
- type: STYLE_TYPE.DYNAMIC,
- options: {
- color: 'Blue to Red',
- colorCategory: 'palette_0',
- fieldMetaOptions: { isEnabled: true, sigma: 3 },
- type: COLOR_MAP_TYPE.ORDINAL,
- field: {
- name: TRANSACTION_DURATION_COUNTRY,
- origin: FIELD_ORIGIN.JOIN,
- },
- useCustomColorRamp: false,
- },
- },
- lineColor: {
- type: STYLE_TYPE.DYNAMIC,
- options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } },
- },
- lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } },
- iconSize: { type: STYLE_TYPE.STATIC, options: { size: 6 } },
- iconOrientation: {
- type: STYLE_TYPE.STATIC,
- options: { orientation: 0 },
- },
- labelText: {
- type: STYLE_TYPE.DYNAMIC,
- options: {
- field: {
- name: TRANSACTION_DURATION_COUNTRY,
- origin: FIELD_ORIGIN.JOIN,
- },
- },
- },
- labelPosition: {
- options: {
- position: LABEL_POSITIONS.CENTER,
- },
- },
- labelZoomRange: {
- options: {
- useLayerZoomRange: true,
- minZoom: 0,
- maxZoom: 24,
- },
- },
- labelColor: {
- type: STYLE_TYPE.STATIC,
- options: { color: '#000000' },
- },
- labelSize: { type: STYLE_TYPE.STATIC, options: { size: 14 } },
- labelBorderColor: {
- type: STYLE_TYPE.STATIC,
- options: { color: '#FFFFFF' },
- },
- symbolizeAs: { options: { value: SYMBOLIZE_AS_TYPES.CIRCLE } },
- labelBorderSize: { options: { size: LABEL_BORDER_SIZES.SMALL } },
- },
- isTimeAware: true,
- };
-}
-
-export async function getLayerList(maps?: MapsStartApi) {
- const basemapLayerDescriptor = maps
- ? await maps.createLayerDescriptors.createBasemapLayerDescriptor()
- : null;
-
- const pageLoadDurationByCountryLayer: VectorLayerDescriptor = {
- joins: [
- {
- leftField: 'iso2',
- right: {
- type: SOURCE_TYPES.ES_TERM_SOURCE,
- id: FIELD_NAME,
- term: CLIENT_GEO_COUNTRY_ISO_CODE,
- metrics: [
- {
- type: AGG_TYPE.AVG,
- field: TRANSACTION_DURATION,
- label: i18n.translate(
- 'xpack.apm.serviceOverview.embeddedMap.metric.label',
- {
- defaultMessage: 'Page load duration',
- }
- ),
- },
- ],
- indexPatternId: APM_STATIC_DATA_VIEW_ID,
- applyGlobalQuery: true,
- applyGlobalTime: true,
- applyForceRefresh: true,
- },
- },
- ],
- sourceDescriptor: {
- type: SOURCE_TYPES.EMS_FILE,
- id: 'world_countries',
- tooltipProperties: [COUNTRY_NAME],
- },
- style: getLayerStyle(),
- id: uuidv4(),
- label: null,
- minZoom: 0,
- maxZoom: 24,
- alpha: 0.75,
- visible: true,
- type: LAYER_TYPE.GEOJSON_VECTOR,
- };
-
- return [
- ...(basemapLayerDescriptor ? [basemapLayerDescriptor] : []),
- pageLoadDurationByCountryLayer,
- ] as BaseLayerDescriptor[];
-}
diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/index.tsx b/x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/index.tsx
deleted file mode 100644
index 6ac672dea1f06..0000000000000
--- a/x-pack/plugins/apm/public/components/app/mobile/service_overview/latency_map/index.tsx
+++ /dev/null
@@ -1,62 +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 {
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiTitle,
- EuiIconTip,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import type { Filter } from '@kbn/es-query';
-import { EmbeddedMap } from './embedded_map';
-
-export function LatencyMap({
- start,
- end,
- kuery,
- filters,
- comparisonEnabled,
-}: {
- start: string;
- end: string;
- kuery?: string;
- filters: Filter[];
- comparisonEnabled: boolean;
-}) {
- return (
- <>
-
-
-
-
- {i18n.translate('xpack.apm.serviceOverview.embeddedMap.title', {
- defaultMessage: 'Average latency per country',
- })}
-
-
-
-
- {comparisonEnabled && (
-
- )}
-
-
-
-
- >
- );
-}
diff --git a/x-pack/plugins/apm/public/hooks/use_filters_for_embeddable_charts.ts b/x-pack/plugins/apm/public/hooks/use_filters_for_embeddable_charts.ts
index 22dbf955b2308..b95d569ca8415 100644
--- a/x-pack/plugins/apm/public/hooks/use_filters_for_embeddable_charts.ts
+++ b/x-pack/plugins/apm/public/hooks/use_filters_for_embeddable_charts.ts
@@ -6,7 +6,7 @@
*/
import { useMemo } from 'react';
-import { SERVICE_NAME, TRANSACTION_TYPE } from '../../common/es_fields/apm';
+import { SERVICE_NAME } from '../../common/es_fields/apm';
import { termQuery } from '../../common/utils/term_query';
import { useApmParams } from './use_apm_params';
import { environmentQuery } from '../../common/utils/environment_query';
@@ -14,19 +14,18 @@ import { environmentQuery } from '../../common/utils/environment_query';
export function useFiltersForEmbeddableCharts() {
const {
path: { serviceName },
- query: { environment, transactionType },
+ query: { environment },
} = useApmParams('/mobile-services/{serviceName}/overview');
return useMemo(
() =>
[
...termQuery(SERVICE_NAME, serviceName),
- ...termQuery(TRANSACTION_TYPE, transactionType),
...environmentQuery(environment),
].map((query) => ({
meta: {},
query,
})),
- [environment, transactionType, serviceName]
+ [environment, serviceName]
);
}
diff --git a/x-pack/plugins/apm/server/routes/mobile/get_mobile_http_requests.ts b/x-pack/plugins/apm/server/routes/mobile/get_mobile_http_requests.ts
index f5821692c11d6..9ec1dc3946e1b 100644
--- a/x-pack/plugins/apm/server/routes/mobile/get_mobile_http_requests.ts
+++ b/x-pack/plugins/apm/server/routes/mobile/get_mobile_http_requests.ts
@@ -17,11 +17,16 @@ import {
SPAN_TYPE,
SPAN_SUBTYPE,
} from '../../../common/es_fields/apm';
+import {
+ MobileSpanSubtype,
+ MobileSpanType,
+} from '../../../common/mobile/constants';
import { environmentQuery } from '../../../common/utils/environment_query';
import { getBucketSize } from '../../../common/utils/get_bucket_size';
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate';
import { Maybe } from '../../../typings/common';
+
import { Coordinate } from '../../../typings/timeseries';
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
@@ -64,7 +69,7 @@ async function getHttpRequestsTimeseries({
const aggs = {
requests: {
- filter: { term: { [SPAN_SUBTYPE]: 'http' } },
+ filter: { term: { [SPAN_SUBTYPE]: MobileSpanSubtype.Http } },
},
};
@@ -76,7 +81,7 @@ async function getHttpRequestsTimeseries({
query: {
bool: {
filter: [
- { exists: { field: SPAN_SUBTYPE } },
+ ...termQuery(SPAN_TYPE, MobileSpanType.External),
...termQuery(SERVICE_NAME, serviceName),
...termQuery(SPAN_TYPE, 'external'),
...termQuery(TRANSACTION_NAME, transactionName),
diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts
index 2ecc7143f4a89..4eaf633fb98c7 100644
--- a/x-pack/plugins/cases/common/api/cases/case.ts
+++ b/x-pack/plugins/cases/common/api/cases/case.ts
@@ -8,7 +8,7 @@
import * as rt from 'io-ts';
import { NumberFromString } from '../saved_object';
-import { UserRT } from '../user';
+import { UserRt } from '../user';
import { CommentResponseRt } from './comment';
import { CasesStatusResponseRt, CaseStatusRt } from './status';
import { CaseConnectorRt } from '../connectors/connector';
@@ -97,7 +97,7 @@ export const CaseUserActionExternalServiceRt = rt.type({
external_title: rt.string,
external_url: rt.string,
pushed_at: rt.string,
- pushed_by: UserRT,
+ pushed_by: UserRt,
});
export const CaseExternalServiceBasicRt = rt.intersection([
@@ -114,12 +114,12 @@ export const CaseAttributesRt = rt.intersection([
rt.type({
duration: rt.union([rt.number, rt.null]),
closed_at: rt.union([rt.string, rt.null]),
- closed_by: rt.union([UserRT, rt.null]),
+ closed_by: rt.union([UserRt, rt.null]),
created_at: rt.string,
- created_by: UserRT,
+ created_by: UserRt,
external_service: CaseFullExternalServiceRt,
updated_at: rt.union([rt.string, rt.null]),
- updated_by: rt.union([UserRT, rt.null]),
+ updated_by: rt.union([UserRt, rt.null]),
}),
]);
diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts
index b768b8f0b593f..2788d1c4022d2 100644
--- a/x-pack/plugins/cases/common/api/cases/comment.ts
+++ b/x-pack/plugins/cases/common/api/cases/comment.ts
@@ -9,16 +9,16 @@ import * as rt from 'io-ts';
import { jsonValueRt } from '../runtime_types';
import { SavedObjectFindOptionsRt } from '../saved_object';
-import { UserRT } from '../user';
+import { UserRt } from '../user';
export const CommentAttributesBasicRt = rt.type({
created_at: rt.string,
- created_by: UserRT,
+ created_by: UserRt,
owner: rt.string,
pushed_at: rt.union([rt.string, rt.null]),
- pushed_by: rt.union([UserRT, rt.null]),
+ pushed_by: rt.union([UserRt, rt.null]),
updated_at: rt.union([rt.string, rt.null]),
- updated_by: rt.union([UserRT, rt.null]),
+ updated_by: rt.union([UserRt, rt.null]),
});
export enum CommentType {
diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts
index bf67624df8508..e6f62a1b8dd01 100644
--- a/x-pack/plugins/cases/common/api/cases/configure.ts
+++ b/x-pack/plugins/cases/common/api/cases/configure.ts
@@ -7,7 +7,7 @@
import * as rt from 'io-ts';
-import { UserRT } from '../user';
+import { UserRt } from '../user';
import { CaseConnectorRt, ConnectorMappingsRt } from '../connectors';
// TODO: we will need to add this type rt.literal('close-by-third-party')
@@ -44,9 +44,9 @@ export const CaseConfigureAttributesRt = rt.intersection([
CasesConfigureBasicRt,
rt.type({
created_at: rt.string,
- created_by: UserRT,
+ created_by: UserRt,
updated_at: rt.union([rt.string, rt.null]),
- updated_by: rt.union([UserRT, rt.null]),
+ updated_by: rt.union([UserRt, rt.null]),
}),
]);
diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts
index 271eec5c6f363..fac254e33094a 100644
--- a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts
+++ b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts
@@ -6,7 +6,7 @@
*/
import * as rt from 'io-ts';
-import { UserRT } from '../../user';
+import { UserRt } from '../../user';
/**
* These values are used in a number of places including to define the accepted values in the
@@ -42,7 +42,7 @@ export const ActionsRt = rt.keyof(Actions);
export const UserActionCommonAttributesRt = rt.type({
created_at: rt.string,
- created_by: UserRT,
+ created_by: UserRt,
owner: rt.string,
action: ActionsRt,
});
diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts
index 746f2bd6972ab..db5a333c7546e 100644
--- a/x-pack/plugins/cases/common/api/helpers.ts
+++ b/x-pack/plugins/cases/common/api/helpers.ts
@@ -19,6 +19,7 @@ import {
INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL,
INTERNAL_BULK_GET_ATTACHMENTS_URL,
INTERNAL_CONNECTORS_URL,
+ INTERNAL_CASE_USERS_URL,
} from '../constants';
export const getCaseDetailsUrl = (id: string): string => {
@@ -72,3 +73,7 @@ export const getCaseBulkGetAttachmentsUrl = (id: string): string => {
export const getCaseConnectorsUrl = (id: string): string => {
return INTERNAL_CONNECTORS_URL.replace('{case_id}', id);
};
+
+export const getCaseUsersUrl = (id: string): string => {
+ return INTERNAL_CASE_USERS_URL.replace('{case_id}', id);
+};
diff --git a/x-pack/plugins/cases/common/api/user.ts b/x-pack/plugins/cases/common/api/user.ts
index 63280d230b777..420e3248faa9c 100644
--- a/x-pack/plugins/cases/common/api/user.ts
+++ b/x-pack/plugins/cases/common/api/user.ts
@@ -7,7 +7,7 @@
import * as rt from 'io-ts';
-export const UserRT = rt.intersection([
+export const UserRt = rt.intersection([
rt.type({
email: rt.union([rt.undefined, rt.null, rt.string]),
full_name: rt.union([rt.undefined, rt.null, rt.string]),
@@ -16,6 +16,14 @@ export const UserRT = rt.intersection([
rt.partial({ profile_uid: rt.string }),
]);
-export const UsersRt = rt.array(UserRT);
+export const UsersRt = rt.array(UserRt);
-export type User = rt.TypeOf;
+export type User = rt.TypeOf;
+
+export const GetCaseUsersResponseRt = rt.type({
+ assignees: rt.array(UserRt),
+ unassignedUsers: rt.array(UserRt),
+ participants: rt.array(UserRt),
+});
+
+export type GetCaseUsersResponse = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts
index c359353f80b80..601599104641b 100644
--- a/x-pack/plugins/cases/common/constants.ts
+++ b/x-pack/plugins/cases/common/constants.ts
@@ -96,6 +96,7 @@ export const INTERNAL_CONNECTORS_URL = `${CASES_INTERNAL_URL}/{case_id}/_connect
export const INTERNAL_BULK_GET_CASES_URL = `${CASES_INTERNAL_URL}/_bulk_get` as const;
export const INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL =
`${CASES_INTERNAL_URL}/{case_id}/user_actions/_stats` as const;
+export const INTERNAL_CASE_USERS_URL = `${CASES_INTERNAL_URL}/{case_id}/_users` as const;
/**
* Action routes
@@ -119,18 +120,21 @@ export const GENERAL_CASES_OWNER = APP_ID;
export const OWNER_INFO = {
[SECURITY_SOLUTION_OWNER]: {
+ id: SECURITY_SOLUTION_OWNER,
appId: 'securitySolutionUI',
label: 'Security',
iconType: 'logoSecurity',
appRoute: '/app/security',
},
[OBSERVABILITY_OWNER]: {
+ id: OBSERVABILITY_OWNER,
appId: 'observability-overview',
label: 'Observability',
iconType: 'logoObservability',
appRoute: '/app/observability',
},
[GENERAL_CASES_OWNER]: {
+ id: GENERAL_CASES_OWNER,
appId: 'management',
label: 'Stack',
iconType: 'casesApp',
diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
index 909bb1dd24ea0..e1e6e5ec42de8 100644
--- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
@@ -426,6 +426,20 @@ describe.skip('AllCasesListGeneric', () => {
});
});
+ it('should render only Name, CreatedOn and Severity columns when isSelectorView=true', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ await waitFor(() => {
+ expect(wrapper.find('[data-test-subj="tableHeaderCell_title_0"]').exists()).toBe(true);
+ expect(wrapper.find('[data-test-subj="tableHeaderCell_createdAt_1"]').exists()).toBe(true);
+ expect(wrapper.find('[data-test-subj="tableHeaderCell_severity_2"]').exists()).toBe(true);
+ expect(wrapper.find('[data-test-subj="tableHeaderCell_assignees_1"]').exists()).toBe(false);
+ });
+ });
+
it('should sort by severity', async () => {
const result = appMockRenderer.render( );
@@ -698,12 +712,12 @@ describe.skip('AllCasesListGeneric', () => {
queryParams: DEFAULT_QUERY_PARAMS,
});
- userEvent.click(getByTestId('options-filter-popover-button-Solution'));
+ userEvent.click(getByTestId('solution-filter-popover-button'));
await waitForEuiPopoverOpen();
userEvent.click(
- getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
+ getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
undefined,
{
skipPointerEventsCheck: true,
@@ -725,7 +739,7 @@ describe.skip('AllCasesListGeneric', () => {
});
userEvent.click(
- getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
+ getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
undefined,
{
skipPointerEventsCheck: true,
@@ -754,7 +768,7 @@ describe.skip('AllCasesListGeneric', () => {
);
- expect(queryByTestId('options-filter-popover-button-Solution')).toBeFalsy();
+ expect(queryByTestId('solution-filter-popover-button')).toBeFalsy();
});
it('should call useGetCases with the correct owner on initial render', async () => {
diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
index ab50f16083f8b..7666d518fe9b4 100644
--- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
@@ -14,11 +14,13 @@ import styled, { css } from 'styled-components';
import type { Case, CaseStatusWithAllStatus, FilterOptions } from '../../../common/ui/types';
import { SortFieldCase, StatusAll } from '../../../common/ui/types';
import { CaseStatuses, caseStatuses } from '../../../common/api';
+import { OWNER_INFO } from '../../../common/constants';
+import type { CasesOwners } from '../../client/helpers/can_use_cases';
import { useAvailableCasesOwners } from '../app/use_available_owners';
import { useCasesColumns } from './use_cases_columns';
import { CasesTableFilters } from './table_filters';
-import type { EuiBasicTableOnChange } from './types';
+import type { EuiBasicTableOnChange, Solution } from './types';
import { CasesTable } from './table';
import { useCasesContext } from '../cases_context/use_cases_context';
@@ -48,6 +50,17 @@ const getSortField = (field: string): SortFieldCase =>
// @ts-ignore
SortFieldCase[field] ?? SortFieldCase.title;
+const isValidSolution = (solution: string): solution is CasesOwners =>
+ Object.keys(OWNER_INFO).includes(solution);
+
+const mapToReadableSolutionName = (solution: string): Solution => {
+ if (isValidSolution(solution)) {
+ return OWNER_INFO[solution];
+ }
+
+ return { id: solution, label: solution, iconType: '' };
+};
+
export interface AllCasesListProps {
hiddenStatuses?: CaseStatusWithAllStatus[];
isSelectorView?: boolean;
@@ -228,6 +241,10 @@ export const AllCasesList = React.memo(
[]
);
+ const availableSolutionsLabels = availableSolutions.map((solution) =>
+ mapToReadableSolutionName(solution)
+ );
+
return (
<>
(
countOpenCases={data.countOpenCases}
countInProgressCases={data.countInProgressCases}
onFilterChanged={onFilterChangedCallback}
- availableSolutions={hasOwner ? [] : availableSolutions}
+ availableSolutions={hasOwner ? [] : availableSolutionsLabels}
initial={{
search: filterOptions.search,
searchFields: filterOptions.searchFields,
@@ -254,8 +271,8 @@ export const AllCasesList = React.memo(
severity: filterOptions.severity,
}}
hiddenStatuses={hiddenStatuses}
- displayCreateCaseButton={isSelectorView}
onCreateCasePressed={onRowClick}
+ isSelectorView={isSelectorView}
isLoading={isLoadingCurrentUserProfile}
currentUserProfile={currentUserProfile}
/>
diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
index 0ba5a65bf3207..c04ea59dfebc5 100644
--- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
@@ -7,7 +7,7 @@
import React, { useState, useCallback } from 'react';
import {
- EuiButton,
+ EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
@@ -29,7 +29,7 @@ export interface AllCasesSelectorModalProps {
const Modal = styled(EuiModal)`
${({ theme }) => `
- min-width: ${theme.eui.euiBreakpoints.l};
+ min-width: ${theme.eui.euiBreakpoints.m};
max-width: ${theme.eui.euiBreakpoints.xl};
`}
`;
@@ -68,13 +68,13 @@ export const AllCasesSelectorModal = React.memo(
/>
-
{i18n.CANCEL}
-
+
diff --git a/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx
new file mode 100644
index 0000000000000..dcb469448b03f
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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 { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
+
+import type { AppMockRenderer } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
+import type { Solution } from './types';
+import {
+ OWNER_INFO,
+ SECURITY_SOLUTION_OWNER,
+ OBSERVABILITY_OWNER,
+} from '../../../common/constants';
+
+import { SolutionFilter } from './solution_filter';
+import userEvent from '@testing-library/user-event';
+
+describe('SolutionFilter ', () => {
+ let appMockRender: AppMockRenderer;
+ const onSelectedOptionsChanged = jest.fn();
+ const solutions: Solution[] = [
+ {
+ id: SECURITY_SOLUTION_OWNER,
+ label: OWNER_INFO[SECURITY_SOLUTION_OWNER].label,
+ iconType: OWNER_INFO[SECURITY_SOLUTION_OWNER].iconType,
+ },
+ {
+ id: OBSERVABILITY_OWNER,
+ label: OWNER_INFO[OBSERVABILITY_OWNER].label,
+ iconType: OWNER_INFO[OBSERVABILITY_OWNER].iconType,
+ },
+ ];
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ jest.clearAllMocks();
+ });
+
+ it('renders button correctly', () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ expect(getByTestId('solution-filter-popover-button')).toBeInTheDocument();
+ });
+
+ it('renders empty label correctly', async () => {
+ const { getByTestId, getByText } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
+
+ expect(getByText('No options available')).toBeInTheDocument();
+ });
+
+ it('renders options correctly', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ expect(getByTestId('solution-filter-popover-button')).toBeInTheDocument();
+
+ userEvent.click(getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
+
+ expect(getByTestId(`solution-filter-popover-item-${solutions[0].id}`)).toBeInTheDocument();
+ expect(getByTestId(`solution-filter-popover-item-${solutions[0].id}`)).toBeInTheDocument();
+ });
+
+ it('should call onSelectionChange with selected solution id', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(getByTestId(`solution-filter-popover-item-${solutions[0].id}`));
+
+ expect(onSelectedOptionsChanged).toHaveBeenCalledWith([solutions[0].id]);
+ });
+
+ it('should call onSelectionChange with empty array when solution option is deselected', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(getByTestId(`solution-filter-popover-item-${solutions[1].id}`));
+
+ expect(onSelectedOptionsChanged).toHaveBeenCalledWith([]);
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx
new file mode 100644
index 0000000000000..b776895e2fe9e
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx
@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useState } from 'react';
+import {
+ EuiFilterButton,
+ EuiFilterSelectItem,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiPopover,
+ EuiText,
+ EuiIcon,
+} from '@elastic/eui';
+import styled from 'styled-components';
+
+import * as i18n from './translations';
+import type { Solution } from './types';
+
+interface FilterPopoverProps {
+ onSelectedOptionsChanged: (value: string[]) => void;
+ options: Solution[];
+ optionsEmptyLabel?: string;
+ selectedOptions: string[];
+}
+
+const ScrollableDiv = styled.div`
+ max-height: 250px;
+ overflow: auto;
+`;
+
+const toggleSelectedGroup = (group: string, selectedGroups: string[]): string[] => {
+ const selectedGroupIndex = selectedGroups.indexOf(group);
+ if (selectedGroupIndex >= 0) {
+ return [
+ ...selectedGroups.slice(0, selectedGroupIndex),
+ ...selectedGroups.slice(selectedGroupIndex + 1),
+ ];
+ }
+ return [...selectedGroups, group];
+};
+
+/**
+ * Popover for selecting a field to filter on
+ *
+ * @param buttonLabel label on dropdwon button
+ * @param onSelectedOptionsChanged change listener to be notified when option selection changes
+ * @param options to display for filtering
+ * @param optionsEmptyLabel shows when options empty
+ * @param selectedOptions manage state of selectedOptions
+ */
+export const SolutionFilterComponent = ({
+ onSelectedOptionsChanged,
+ options,
+ optionsEmptyLabel,
+ selectedOptions,
+}: FilterPopoverProps) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const setIsPopoverOpenCb = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
+ const toggleSelectedGroupCb = useCallback(
+ (option) => onSelectedOptionsChanged(toggleSelectedGroup(option, selectedOptions)),
+ [selectedOptions, onSelectedOptionsChanged]
+ );
+
+ return (
+ 0}
+ numActiveFilters={selectedOptions.length}
+ aria-label={i18n.SOLUTION}
+ >
+ {i18n.SOLUTION}
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={setIsPopoverOpenCb}
+ panelPaddingSize="none"
+ repositionOnScroll
+ >
+
+ {options.map((option, index) => (
+
+
+
+
+
+ {option.label}
+
+
+ ))}
+
+ {options.length === 0 && optionsEmptyLabel != null && (
+
+
+
+ {optionsEmptyLabel}
+
+
+
+ )}
+
+ );
+};
+
+SolutionFilterComponent.displayName = 'SolutionFilterComponent';
+
+export const SolutionFilter = React.memo(SolutionFilterComponent);
+
+SolutionFilter.displayName = 'SolutionFilter';
diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx
index 6c0ce7569b1c6..ccfcf715a67ba 100644
--- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx
@@ -6,16 +6,20 @@
*/
import React from 'react';
-import { mount } from 'enzyme';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
+import { waitForComponentToUpdate } from '../../common/test_utils';
import { CaseStatuses } from '../../../common/api';
-import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
+import {
+ OWNER_INFO,
+ SECURITY_SOLUTION_OWNER,
+ OBSERVABILITY_OWNER,
+} from '../../../common/constants';
import type { AppMockRenderer } from '../../common/mock';
-import { createAppMockRenderer, TestProviders } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases';
import { CasesTableFilters } from './table_filters';
import { useGetTags } from '../../containers/use_get_tags';
@@ -52,37 +56,31 @@ describe('CasesTableFilters ', () => {
});
it('should render the case status filter dropdown', () => {
- const wrapper = mount(
-
-
-
- );
+ appMockRender.render( );
- expect(wrapper.find(`[data-test-subj="case-status-filter"]`).first().exists()).toBeTruthy();
+ expect(screen.getByTestId('case-status-filter')).toBeInTheDocument();
});
it('should render the case severity filter dropdown', () => {
- const result = appMockRender.render( );
- expect(result.getByTestId('case-severity-filter')).toBeTruthy();
+ appMockRender.render( );
+ expect(screen.getByTestId('case-severity-filter')).toBeTruthy();
});
it('should call onFilterChange when the severity filter changes', async () => {
- const result = appMockRender.render( );
- userEvent.click(result.getByTestId('case-severity-filter'));
+ appMockRender.render( );
+ userEvent.click(screen.getByTestId('case-severity-filter'));
await waitForEuiPopoverOpen();
- userEvent.click(result.getByTestId('case-severity-filter-high'));
+ userEvent.click(screen.getByTestId('case-severity-filter-high'));
expect(onFilterChanged).toBeCalledWith({ severity: 'high' });
});
- it('should call onFilterChange when selected tags change', () => {
- const wrapper = mount(
-
-
-
- );
- wrapper.find(`[data-test-subj="options-filter-popover-button-Tags"]`).last().simulate('click');
- wrapper.find(`[data-test-subj="options-filter-popover-item-coke"]`).last().simulate('click');
+ it('should call onFilterChange when selected tags change', async () => {
+ appMockRender.render( );
+
+ userEvent.click(screen.getByTestId('options-filter-popover-button-Tags'));
+ await waitForEuiPopoverOpen();
+ userEvent.click(screen.getByTestId('options-filter-popover-item-coke'));
expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] });
});
@@ -109,29 +107,21 @@ describe('CasesTableFilters ', () => {
`);
});
- it('should call onFilterChange when search changes', () => {
- const wrapper = mount(
-
-
-
- );
-
- wrapper
- .find(`[data-test-subj="search-cases"]`)
- .last()
- .simulate('keyup', { key: 'Enter', target: { value: 'My search' } });
+ it('should call onFilterChange when search changes', async () => {
+ appMockRender.render( );
+
+ await userEvent.type(screen.getByTestId('search-cases'), 'My search{enter}');
+
expect(onFilterChanged).toBeCalledWith({ search: 'My search' });
});
- it('should call onFilterChange when changing status', () => {
- const wrapper = mount(
-
-
-
- );
+ it('should call onFilterChange when changing status', async () => {
+ appMockRender.render( );
+
+ userEvent.click(screen.getByTestId('case-status-filter'));
+ await waitForEuiPopoverOpen();
+ userEvent.click(screen.getByTestId('case-status-filter-closed'));
- wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
- wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click');
expect(onFilterChanged).toBeCalledWith({ status: CaseStatuses.closed });
});
@@ -143,11 +133,8 @@ describe('CasesTableFilters ', () => {
tags: ['pepsi', 'rc'],
},
};
- mount(
-
-
-
- );
+
+ appMockRender.render( );
expect(onFilterChanged).toHaveBeenCalledWith({ tags: ['pepsi'] });
});
@@ -186,165 +173,104 @@ describe('CasesTableFilters ', () => {
});
it('StatusFilterWrapper should have a fixed width of 180px', () => {
- const wrapper = mount(
-
-
-
- );
-
- expect(wrapper.find('[data-test-subj="status-filter-wrapper"]').first()).toHaveStyleRule(
- 'flex-basis',
- '180px',
- {
- modifier: '&&',
- }
- );
+ appMockRender.render( );
+
+ expect(screen.getByTestId('status-filter-wrapper')).toHaveStyleRule('flex-basis', '180px', {
+ modifier: '&&',
+ });
});
describe('Solution filter', () => {
+ const securitySolution = {
+ id: SECURITY_SOLUTION_OWNER,
+ label: OWNER_INFO[SECURITY_SOLUTION_OWNER].label,
+ iconType: OWNER_INFO[SECURITY_SOLUTION_OWNER].iconType,
+ };
+ const observabilitySolution = {
+ id: OBSERVABILITY_OWNER,
+ label: OWNER_INFO[OBSERVABILITY_OWNER].label,
+ iconType: OWNER_INFO[OBSERVABILITY_OWNER].iconType,
+ };
+
it('shows Solution filter when provided more than 1 availableSolutions', () => {
- const wrapper = mount(
-
-
-
+ appMockRender.render(
+
);
- expect(
- wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).exists()
- ).toBeTruthy();
+ expect(screen.getByTestId('solution-filter-popover-button')).toBeInTheDocument();
});
it('does not show Solution filter when provided less than 1 availableSolutions', () => {
- const wrapper = mount(
-
-
-
+ appMockRender.render(
+
);
- expect(
- wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).exists()
- ).toBeFalsy();
+ expect(screen.queryByTestId('solution-filter-popover-button')).not.toBeInTheDocument();
});
- it('should call onFilterChange when selected solution changes', () => {
- const wrapper = mount(
-
-
-
+ it('should call onFilterChange when selected solution changes', async () => {
+ appMockRender.render(
+
);
- wrapper
- .find(`[data-test-subj="options-filter-popover-button-Solution"]`)
- .last()
- .simulate('click');
+ userEvent.click(screen.getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
- wrapper
- .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`)
- .last()
- .simulate('click');
+ userEvent.click(
+ screen.getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`)
+ );
expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] });
});
- it('should deselect all solutions', () => {
- const wrapper = mount(
-
-
-
+ it('should deselect all solutions', async () => {
+ appMockRender.render(
+
);
- wrapper
- .find(`[data-test-subj="options-filter-popover-button-Solution"]`)
- .last()
- .simulate('click');
+ userEvent.click(screen.getByTestId('solution-filter-popover-button'));
- wrapper
- .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`)
- .last()
- .simulate('click');
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(
+ screen.getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`)
+ );
expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] });
- wrapper
- .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`)
- .last()
- .simulate('click');
+ userEvent.click(
+ screen.getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`)
+ );
expect(onFilterChanged).toBeCalledWith({ owner: [] });
});
it('does not select a solution on initial render', () => {
- const wrapper = mount(
-
-
-
- );
-
- expect(
- wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).first().props()
- ).toEqual(expect.objectContaining({ hasActiveFilters: false }));
- });
- });
-
- describe('create case button', () => {
- it('should not render the create case button when displayCreateCaseButton and onCreateCasePressed are not passed', () => {
- const wrapper = mount(
-
-
-
+ appMockRender.render(
+
);
- expect(wrapper.find(`[data-test-subj="cases-table-add-case-filter-bar"]`).length).toBe(0);
- });
-
- it('should render the create case button when displayCreateCaseButton and onCreateCasePressed are passed', () => {
- const onCreateCasePressed = jest.fn();
- const wrapper = mount(
-
-
-
- );
- expect(wrapper.find(`[data-test-subj="cases-table-add-case-filter-bar"]`)).toBeTruthy();
- });
- it('should call the onCreateCasePressed when create case is clicked', () => {
- const onCreateCasePressed = jest.fn();
- const wrapper = mount(
-
-
-
+ expect(screen.getByTestId('solution-filter-popover-button')).not.toHaveAttribute(
+ 'hasActiveFilters'
);
- wrapper
- .find(`button[data-test-subj="cases-table-add-case-filter-bar"]`)
- .first()
- .simulate('click');
- wrapper.update();
- // NOTE: intentionally checking no arguments are passed
- expect(onCreateCasePressed).toHaveBeenCalledWith();
});
});
describe('assignees filter', () => {
it('should hide the assignees filters on basic license', async () => {
- const result = appMockRender.render( );
+ appMockRender.render( );
- expect(result.queryByTestId('options-filter-popover-button-assignees')).toBeNull();
+ expect(screen.queryByTestId('options-filter-popover-button-assignees')).toBeNull();
});
it('should show the assignees filters on platinum license', async () => {
@@ -353,9 +279,45 @@ describe('CasesTableFilters ', () => {
});
appMockRender = createAppMockRenderer({ license });
- const result = appMockRender.render( );
+ appMockRender.render( );
+
+ expect(screen.getByTestId('options-filter-popover-button-assignees')).toBeInTheDocument();
+ });
+ });
+
+ describe('create case button', () => {
+ it('should not render the create case button when isSelectorView is false and onCreateCasePressed are not passed', () => {
+ appMockRender.render( );
+ expect(screen.queryByTestId('cases-table-add-case-filter-bar')).not.toBeInTheDocument();
+ });
+
+ it('should render the create case button when isSelectorView is true and onCreateCasePressed are passed', () => {
+ const onCreateCasePressed = jest.fn();
+ appMockRender.render(
+
+ );
+ expect(screen.getByTestId('cases-table-add-case-filter-bar')).toBeInTheDocument();
+ });
- expect(result.getByTestId('options-filter-popover-button-assignees')).toBeInTheDocument();
+ it('should call the onCreateCasePressed when create case is clicked', async () => {
+ const onCreateCasePressed = jest.fn();
+ appMockRender.render(
+
+ );
+
+ userEvent.click(screen.getByTestId('cases-table-add-case-filter-bar'));
+
+ await waitForComponentToUpdate();
+ // NOTE: intentionally checking no arguments are passed
+ expect(onCreateCasePressed).toHaveBeenCalledWith();
});
});
});
diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx
index 5032922a06d12..41c46d5137e98 100644
--- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx
@@ -15,6 +15,7 @@ import { StatusAll } from '../../../common/ui/types';
import { CaseStatuses } from '../../../common/api';
import type { FilterOptions } from '../../containers/types';
import { FilterPopover } from '../filter_popover';
+import { SolutionFilter } from './solution_filter';
import { StatusFilter } from './status_filter';
import * as i18n from './translations';
import { SeverityFilter } from './severity_filter';
@@ -24,6 +25,7 @@ import { AssigneesFilterPopover } from './assignees_filter';
import type { CurrentUserProfile } from '../types';
import { useCasesFeatures } from '../../common/use_cases_features';
import type { AssigneesFilteringSelection } from '../user_profiles/types';
+import type { Solution } from './types';
interface CasesTableFiltersProps {
countClosedCases: number | null;
@@ -32,8 +34,8 @@ interface CasesTableFiltersProps {
onFilterChanged: (filterOptions: Partial) => void;
initial: FilterOptions;
hiddenStatuses?: CaseStatusWithAllStatus[];
- availableSolutions: string[];
- displayCreateCaseButton?: boolean;
+ availableSolutions: Solution[];
+ isSelectorView?: boolean;
onCreateCasePressed?: () => void;
isLoading: boolean;
currentUserProfile: CurrentUserProfile;
@@ -60,7 +62,7 @@ const CasesTableFiltersComponent = ({
initial = DEFAULT_FILTER_OPTIONS,
hiddenStatuses,
availableSolutions,
- displayCreateCaseButton,
+ isSelectorView = false,
onCreateCasePressed,
isLoading,
currentUserProfile,
@@ -156,6 +158,18 @@ const CasesTableFiltersComponent = ({
+ {isSelectorView && onCreateCasePressed ? (
+
+
+ {i18n.CREATE_CASE_TITLE}
+
+
+ ) : null}
- {caseAssignmentAuthorized ? (
+ {caseAssignmentAuthorized && !isSelectorView ? (
{availableSolutions.length > 1 && (
-
- {displayCreateCaseButton && onCreateCasePressed ? (
-
-
- {i18n.CREATE_CASE_TITLE}
-
-
- ) : null}
);
};
diff --git a/x-pack/plugins/cases/public/components/all_cases/types.ts b/x-pack/plugins/cases/public/components/all_cases/types.ts
index 5014522177570..7cf9c410ec073 100644
--- a/x-pack/plugins/cases/public/components/all_cases/types.ts
+++ b/x-pack/plugins/cases/public/components/all_cases/types.ts
@@ -24,3 +24,8 @@ export interface EuiBasicTableOnChange {
};
sort?: EuiBasicTableSortTypes;
}
+export interface Solution {
+ id: string;
+ label: string;
+ iconType: string;
+}
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx
index f11794bcf13e2..060ac64cafb08 100644
--- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx
@@ -97,7 +97,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -148,38 +147,7 @@ describe('useCasesColumns ', () => {
"name": "Name",
"render": [Function],
"sortable": true,
- "width": undefined,
- },
- Object {
- "field": "assignees",
- "name": "Assignees",
- "render": [Function],
- "width": undefined,
- },
- Object {
- "field": "tags",
- "name": "Tags",
- "render": [Function],
- "width": undefined,
- },
- Object {
- "align": "right",
- "field": "totalAlerts",
- "name": "Alerts",
- "render": [Function],
- "width": "55px",
- },
- Object {
- "align": "right",
- "field": "owner",
- "name": "Solution",
- "render": [Function],
- },
- Object {
- "align": "right",
- "field": "totalComment",
- "name": "Comments",
- "render": [Function],
+ "width": "55%",
},
Object {
"field": "createdAt",
@@ -187,24 +155,6 @@ describe('useCasesColumns ', () => {
"render": [Function],
"sortable": true,
},
- Object {
- "field": "updatedAt",
- "name": "Updated on",
- "render": [Function],
- "sortable": true,
- "width": "80px",
- },
- Object {
- "name": "External Incident",
- "render": [Function],
- "width": "80px",
- },
- Object {
- "field": "status",
- "name": "Status",
- "render": [Function],
- "sortable": true,
- },
Object {
"field": "severity",
"name": "Severity",
@@ -280,7 +230,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -365,7 +314,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -445,7 +393,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -530,7 +477,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -575,32 +521,7 @@ describe('useCasesColumns ', () => {
"name": "Name",
"render": [Function],
"sortable": true,
- "width": undefined,
- },
- Object {
- "field": "tags",
- "name": "Tags",
- "render": [Function],
- "width": undefined,
- },
- Object {
- "align": "right",
- "field": "totalAlerts",
- "name": "Alerts",
- "render": [Function],
- "width": "55px",
- },
- Object {
- "align": "right",
- "field": "owner",
- "name": "Solution",
- "render": [Function],
- },
- Object {
- "align": "right",
- "field": "totalComment",
- "name": "Comments",
- "render": [Function],
+ "width": "55%",
},
Object {
"field": "createdAt",
@@ -608,24 +529,6 @@ describe('useCasesColumns ', () => {
"render": [Function],
"sortable": true,
},
- Object {
- "field": "updatedAt",
- "name": "Updated on",
- "render": [Function],
- "sortable": true,
- "width": "80px",
- },
- Object {
- "name": "External Incident",
- "render": [Function],
- "width": "80px",
- },
- Object {
- "field": "status",
- "name": "Status",
- "render": [Function],
- "sortable": true,
- },
Object {
"field": "severity",
"name": "Severity",
@@ -657,32 +560,7 @@ describe('useCasesColumns ', () => {
"name": "Name",
"render": [Function],
"sortable": true,
- "width": undefined,
- },
- Object {
- "field": "tags",
- "name": "Tags",
- "render": [Function],
- "width": undefined,
- },
- Object {
- "align": "right",
- "field": "totalAlerts",
- "name": "Alerts",
- "render": [Function],
- "width": "55px",
- },
- Object {
- "align": "right",
- "field": "owner",
- "name": "Solution",
- "render": [Function],
- },
- Object {
- "align": "right",
- "field": "totalComment",
- "name": "Comments",
- "render": [Function],
+ "width": "55%",
},
Object {
"field": "createdAt",
@@ -690,24 +568,6 @@ describe('useCasesColumns ', () => {
"render": [Function],
"sortable": true,
},
- Object {
- "field": "updatedAt",
- "name": "Updated on",
- "render": [Function],
- "sortable": true,
- "width": "80px",
- },
- Object {
- "name": "External Incident",
- "render": [Function],
- "width": "80px",
- },
- Object {
- "field": "status",
- "name": "Status",
- "render": [Function],
- "sortable": true,
- },
Object {
"field": "severity",
"name": "Severity",
@@ -776,7 +636,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
index a2760c71a8ef5..1f267eca40a84 100644
--- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
@@ -118,7 +118,7 @@ export const useCasesColumns = ({
render: (title: string, theCase: Case) => {
if (theCase.id != null && theCase.title != null) {
const caseDetailsLinkComponent = isSelectorView ? (
-
+ theCase.title
) : (
@@ -137,70 +137,72 @@ export const useCasesColumns = ({
}
return getEmptyTagValue();
},
- width: !isSelectorView ? '20%' : undefined,
+ width: !isSelectorView ? '20%' : '55%',
},
];
- if (caseAssignmentAuthorized) {
+ if (caseAssignmentAuthorized && !isSelectorView) {
columns.push({
field: 'assignees',
name: i18n.ASSIGNEES,
render: (assignees: Case['assignees']) => (
),
- width: !isSelectorView ? '180px' : undefined,
+ width: '180px',
});
}
- columns.push({
- field: 'tags',
- name: i18n.TAGS,
- render: (tags: Case['tags']) => {
- if (tags != null && tags.length > 0) {
- const clampedBadges = (
-
- {tags.map((tag: string, i: number) => (
-
- {tag}
-
- ))}
-
- );
+ if (!isSelectorView) {
+ columns.push({
+ field: 'tags',
+ name: i18n.TAGS,
+ render: (tags: Case['tags']) => {
+ if (tags != null && tags.length > 0) {
+ const clampedBadges = (
+
+ {tags.map((tag: string, i: number) => (
+
+ {tag}
+
+ ))}
+
+ );
- const unclampedBadges = (
-
- {tags.map((tag: string, i: number) => (
-
- {tag}
-
- ))}
-
- );
+ const unclampedBadges = (
+
+ {tags.map((tag: string, i: number) => (
+
+ {tag}
+
+ ))}
+
+ );
- return (
-
- {clampedBadges}
-
- );
- }
- return getEmptyTagValue();
- },
- width: !isSelectorView ? '15%' : undefined,
- });
+ return (
+
+ {clampedBadges}
+
+ );
+ }
+ return getEmptyTagValue();
+ },
+ width: '15%',
+ });
+ }
- if (isAlertsEnabled) {
+ if (isAlertsEnabled && !isSelectorView) {
columns.push({
align: RIGHT_ALIGNMENT,
field: 'totalAlerts',
@@ -213,7 +215,7 @@ export const useCasesColumns = ({
});
}
- if (showSolutionColumn) {
+ if (showSolutionColumn && !isSelectorView) {
columns.push({
align: RIGHT_ALIGNMENT,
field: 'owner',
@@ -234,15 +236,17 @@ export const useCasesColumns = ({
});
}
- columns.push({
- align: RIGHT_ALIGNMENT,
- field: 'totalComment',
- name: i18n.COMMENTS,
- render: (totalComment: Case['totalComment']) =>
- totalComment != null
- ? renderStringField(`${totalComment}`, `case-table-column-commentCount`)
- : getEmptyTagValue(),
- });
+ if (!isSelectorView) {
+ columns.push({
+ align: RIGHT_ALIGNMENT,
+ field: 'totalComment',
+ name: i18n.COMMENTS,
+ render: (totalComment: Case['totalComment']) =>
+ totalComment != null
+ ? renderStringField(`${totalComment}`, `case-table-column-commentCount`)
+ : getEmptyTagValue(),
+ });
+ }
if (filterStatus === CaseStatuses.closed) {
columns.push({
@@ -278,67 +282,70 @@ export const useCasesColumns = ({
});
}
+ if (!isSelectorView) {
+ columns.push({
+ field: 'updatedAt',
+ name: i18n.UPDATED_ON,
+ sortable: true,
+ render: (updatedAt: Case['updatedAt']) => {
+ if (updatedAt != null) {
+ return (
+
+
+
+ );
+ }
+ return getEmptyTagValue();
+ },
+ });
+ }
+
+ if (!isSelectorView) {
+ columns.push(
+ {
+ name: i18n.EXTERNAL_INCIDENT,
+ render: (theCase: Case) => {
+ if (theCase.id != null) {
+ return ;
+ }
+ return getEmptyTagValue();
+ },
+ width: isSelectorView ? '80px' : undefined,
+ },
+ {
+ field: 'status',
+ name: i18n.STATUS,
+ sortable: true,
+ render: (status: Case['status']) => {
+ if (status != null) {
+ return ;
+ }
+
+ return getEmptyTagValue();
+ },
+ }
+ );
+ }
columns.push({
- field: 'updatedAt',
- name: i18n.UPDATED_ON,
+ field: 'severity',
+ name: i18n.SEVERITY,
sortable: true,
- render: (updatedAt: Case['updatedAt']) => {
- if (updatedAt != null) {
+ render: (severity: Case['severity']) => {
+ if (severity != null) {
+ const severityData = severities[severity ?? CaseSeverity.LOW];
return (
-
-
-
+
+ {severityData.label}
+
);
}
return getEmptyTagValue();
},
- width: isSelectorView ? '80px' : undefined,
});
- columns.push(
- {
- name: i18n.EXTERNAL_INCIDENT,
- render: (theCase: Case) => {
- if (theCase.id != null) {
- return ;
- }
- return getEmptyTagValue();
- },
- width: isSelectorView ? '80px' : undefined,
- },
- {
- field: 'status',
- name: i18n.STATUS,
- sortable: true,
- render: (status: Case['status']) => {
- if (status != null) {
- return ;
- }
-
- return getEmptyTagValue();
- },
- },
- {
- field: 'severity',
- name: i18n.SEVERITY,
- sortable: true,
- render: (severity: Case['severity']) => {
- if (severity != null) {
- const severityData = severities[severity ?? CaseSeverity.LOW];
- return (
-
- {severityData.label}
-
- );
- }
- return getEmptyTagValue();
- },
- }
- );
-
if (isSelectorView) {
columns.push({
align: RIGHT_ALIGNMENT,
@@ -351,7 +358,6 @@ export const useCasesColumns = ({
assignCaseAction(theCase);
}}
size="s"
- fill={true}
>
{i18n.SELECT}
diff --git a/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx b/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx
new file mode 100644
index 0000000000000..a6c6de8f19770
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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 { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
+
+import type { AppMockRenderer } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
+
+import { FilterPopover } from '.';
+import userEvent from '@testing-library/user-event';
+
+describe('FilterPopover ', () => {
+ let appMockRender: AppMockRenderer;
+ const onSelectedOptionsChanged = jest.fn();
+ const tags: string[] = ['coke', 'pepsi'];
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ jest.clearAllMocks();
+ });
+
+ it('renders button label correctly', () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ expect(getByTestId('options-filter-popover-button-Tags')).toBeInTheDocument();
+ });
+
+ it('renders empty label correctly', async () => {
+ const { getByTestId, getByText } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('options-filter-popover-button-Tags'));
+
+ await waitForEuiPopoverOpen();
+
+ expect(getByText('No options available')).toBeInTheDocument();
+ });
+
+ it('renders string type options correctly', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('options-filter-popover-button-Tags'));
+
+ await waitForEuiPopoverOpen();
+
+ expect(getByTestId(`options-filter-popover-item-${tags[0]}`)).toBeInTheDocument();
+ expect(getByTestId(`options-filter-popover-item-${tags[1]}`)).toBeInTheDocument();
+ });
+
+ it('should call onSelectionChange with selected option', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('options-filter-popover-button-Tags'));
+
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(getByTestId(`options-filter-popover-item-${tags[0]}`));
+
+ expect(onSelectedOptionsChanged).toHaveBeenCalledWith([tags[0]]);
+ });
+
+ it('should call onSelectionChange with empty array when option is deselected', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('options-filter-popover-button-Tags'));
+
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(getByTestId(`options-filter-popover-item-${tags[0]}`));
+
+ expect(onSelectedOptionsChanged).toHaveBeenCalledWith([]);
+ });
+});
diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap
index 193451e9d146a..4c7c643c80fa9 100644
--- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap
+++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap
@@ -2268,6 +2268,90 @@ Object {
}
`;
+exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionUsers" with an error and entity 1`] = `
+Object {
+ "error": Object {
+ "code": "Error",
+ "message": "an error",
+ },
+ "event": Object {
+ "action": "case_user_action_get_users",
+ "category": Array [
+ "database",
+ ],
+ "outcome": "failure",
+ "type": Array [
+ "access",
+ ],
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "1",
+ "type": "cases-user-actions",
+ },
+ },
+ "message": "Failed attempt to access cases-user-actions [id=1] as owner \\"awesome\\"",
+}
+`;
+
+exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionUsers" with an error but no entity 1`] = `
+Object {
+ "error": Object {
+ "code": "Error",
+ "message": "an error",
+ },
+ "event": Object {
+ "action": "case_user_action_get_users",
+ "category": Array [
+ "database",
+ ],
+ "outcome": "failure",
+ "type": Array [
+ "access",
+ ],
+ },
+ "message": "Failed attempt to access a user actions as any owners",
+}
+`;
+
+exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionUsers" without an error but with an entity 1`] = `
+Object {
+ "event": Object {
+ "action": "case_user_action_get_users",
+ "category": Array [
+ "database",
+ ],
+ "outcome": "success",
+ "type": Array [
+ "access",
+ ],
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "5",
+ "type": "cases-user-actions",
+ },
+ },
+ "message": "User has accessed cases-user-actions [id=5] as owner \\"super\\"",
+}
+`;
+
+exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionUsers" without an error or entity 1`] = `
+Object {
+ "event": Object {
+ "action": "case_user_action_get_users",
+ "category": Array [
+ "database",
+ ],
+ "outcome": "success",
+ "type": Array [
+ "access",
+ ],
+ },
+ "message": "User has accessed a user actions as any owners",
+}
+`;
+
exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" with an error and entity 1`] = `
Object {
"error": Object {
diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts
index cbfef6f4713e3..83529478d1d10 100644
--- a/x-pack/plugins/cases/server/authorization/index.ts
+++ b/x-pack/plugins/cases/server/authorization/index.ts
@@ -367,4 +367,12 @@ export const Operations: Record>((acc, profile) => {
- acc.set(profile.uid, profile);
- return acc;
- }, new Map());
+ return userProfiles;
};
diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts
index 06d2f1da63629..6ab60805ad5b1 100644
--- a/x-pack/plugins/cases/server/client/cases/utils.ts
+++ b/x-pack/plugins/cases/server/client/cases/utils.ts
@@ -8,6 +8,7 @@
import { uniqBy, isEmpty } from 'lodash';
import type { UserProfile } from '@kbn/security-plugin/common';
import type { IBasePath } from '@kbn/core-http-browser';
+import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import { CASE_VIEW_PAGE_TABS } from '../../../common/types';
import { isPushedUserAction } from '../../../common/utils/user_actions';
import type {
@@ -430,3 +431,22 @@ export const getDurationForUpdate = ({
};
}
};
+
+export const getUserProfiles = async (
+ securityStartPlugin: SecurityPluginStart,
+ uids: Set
+): Promise> => {
+ if (uids.size <= 0) {
+ return new Map();
+ }
+
+ const userProfiles =
+ (await securityStartPlugin.userProfiles.bulkGet({
+ uids,
+ })) ?? [];
+
+ return userProfiles.reduce>((acc, profile) => {
+ acc.set(profile.uid, profile);
+ return acc;
+ }, new Map());
+};
diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts
index 2c17182f49a07..b7e34a4ea2af5 100644
--- a/x-pack/plugins/cases/server/client/mocks.ts
+++ b/x-pack/plugins/cases/server/client/mocks.ts
@@ -95,6 +95,7 @@ const createUserActionsSubClientMock = (): UserActionsSubClientMock => {
getAll: jest.fn(),
getConnectors: jest.fn(),
stats: jest.fn(),
+ getUsers: jest.fn(),
};
};
diff --git a/x-pack/plugins/cases/server/client/user_actions/client.ts b/x-pack/plugins/cases/server/client/user_actions/client.ts
index 2676955719adb..eb9581be49cff 100644
--- a/x-pack/plugins/cases/server/client/user_actions/client.ts
+++ b/x-pack/plugins/cases/server/client/user_actions/client.ts
@@ -10,12 +10,14 @@ import type {
CaseUserActionStatsResponse,
UserActionFindResponse,
CaseUserActionsDeprecatedResponse,
+ GetCaseUsersResponse,
} from '../../../common/api';
import type { CasesClientArgs } from '../types';
import { get } from './get';
import { getConnectors } from './connectors';
import { getStats } from './stats';
-import type { GetConnectorsRequest, UserActionFind, UserActionGet } from './types';
+import { getUsers } from './users';
+import type { GetConnectorsRequest, UserActionFind, UserActionGet, GetUsersRequest } from './types';
import { find } from './find';
import type { CasesClient } from '../client';
@@ -32,11 +34,14 @@ export interface UserActionsSubClient {
* Retrieves all the connectors used within a given case
*/
getConnectors(params: GetConnectorsRequest): Promise;
-
/**
* Retrieves the total of comments and user actions in a given case
*/
stats(params: UserActionGet): Promise;
+ /**
+ * Retrieves all users participating in a case
+ */
+ getUsers(params: GetUsersRequest): Promise;
}
/**
@@ -51,6 +56,7 @@ export const createUserActionsSubClient = (
getAll: (params) => get(params, clientArgs),
getConnectors: (params) => getConnectors(params, clientArgs),
stats: (params) => getStats(params, casesClient, clientArgs),
+ getUsers: (params) => getUsers(params, casesClient, clientArgs),
};
return Object.freeze(attachmentSubClient);
diff --git a/x-pack/plugins/cases/server/client/user_actions/types.ts b/x-pack/plugins/cases/server/client/user_actions/types.ts
index 5249bc27d1e58..352ade17d62d8 100644
--- a/x-pack/plugins/cases/server/client/user_actions/types.ts
+++ b/x-pack/plugins/cases/server/client/user_actions/types.ts
@@ -18,6 +18,7 @@ export interface UserActionGet {
}
export type GetConnectorsRequest = UserActionGet;
+export type GetUsersRequest = UserActionGet;
export interface UserActionFind {
params: UserActionFindRequest;
diff --git a/x-pack/plugins/cases/server/client/user_actions/users.ts b/x-pack/plugins/cases/server/client/user_actions/users.ts
new file mode 100644
index 0000000000000..3a1bd7a6bb333
--- /dev/null
+++ b/x-pack/plugins/cases/server/client/user_actions/users.ts
@@ -0,0 +1,139 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { UserProfile } from '@kbn/security-plugin/common';
+import type { GetCaseUsersResponse, User } from '../../../common/api';
+import { GetCaseUsersResponseRt } from '../../../common/api';
+import type { OwnerEntity } from '../../authorization';
+import { Operations } from '../../authorization';
+import { createCaseError } from '../../common/error';
+import type { CasesClient } from '../client';
+import type { CasesClientArgs } from '../types';
+import type { GetUsersRequest } from './types';
+import { getUserProfiles } from '../cases/utils';
+
+export const getUsers = async (
+ { caseId }: GetUsersRequest,
+ casesClient: CasesClient,
+ clientArgs: CasesClientArgs
+): Promise => {
+ const {
+ services: { userActionService },
+ logger,
+ authorization,
+ securityStartPlugin,
+ } = clientArgs;
+
+ try {
+ // ensure that we have authorization for reading the case
+ const theCase = await casesClient.cases.resolve({ id: caseId, includeComments: false });
+
+ const { participants, assignedAndUnassignedUsers } = await userActionService.getUsers({
+ caseId,
+ });
+
+ const entities: OwnerEntity[] = participants.map((participant) => ({
+ id: participant.id,
+ owner: participant.owner,
+ }));
+
+ await authorization.ensureAuthorized({
+ entities,
+ operation: Operations.getUserActionUsers,
+ });
+
+ const participantsUids = participants
+ .filter((participant) => participant.user.profile_uid != null)
+ .map((participant) => participant.user.profile_uid) as string[];
+
+ const assigneesUids = theCase.case.assignees.map((assignee) => assignee.uid);
+
+ const userProfileUids = new Set([
+ ...assignedAndUnassignedUsers,
+ ...participantsUids,
+ ...assigneesUids,
+ ]);
+
+ const userProfiles = await getUserProfiles(securityStartPlugin, userProfileUids);
+
+ const participantsResponse = convertUserInfoToResponse(
+ userProfiles,
+ participants.map((participant) => ({
+ uid: participant.user.profile_uid,
+ user: participant.user,
+ }))
+ );
+
+ const assigneesResponse = convertUserInfoToResponse(userProfiles, theCase.case.assignees);
+
+ /**
+ * To avoid duplicates, a user that is
+ * a participant or an assignee should not be
+ * part of the assignedAndUnassignedUsers Set
+ */
+ const unassignedUsers = removeAllFromSet(assignedAndUnassignedUsers, [
+ ...participantsUids,
+ ...assigneesUids,
+ ]);
+
+ const unassignedUsersResponse = convertUserInfoToResponse(
+ userProfiles,
+ [...unassignedUsers.values()].map((uid) => ({
+ uid,
+ }))
+ );
+
+ const results = {
+ participants: participantsResponse,
+ assignees: assigneesResponse,
+ unassignedUsers: unassignedUsersResponse,
+ };
+
+ return GetCaseUsersResponseRt.encode(results);
+ } catch (error) {
+ throw createCaseError({
+ message: `Failed to retrieve the case users case id: ${caseId}: ${error}`,
+ error,
+ logger,
+ });
+ }
+};
+
+const convertUserInfoToResponse = (
+ userProfiles: Map,
+ usersInfo: Array<{ uid: string | undefined; user?: User }>
+): User[] => {
+ const response = [];
+
+ for (const info of usersInfo) {
+ response.push(getUserInformation(userProfiles, info.uid, info.user));
+ }
+
+ return response;
+};
+
+const getUserInformation = (
+ userProfiles: Map,
+ uid: string | undefined,
+ userInfo?: User
+): User => {
+ const userProfile = uid != null ? userProfiles.get(uid) : undefined;
+
+ return {
+ email: userProfile?.user.email ?? userInfo?.email,
+ full_name: userProfile?.user.full_name ?? userInfo?.full_name,
+ username: userProfile?.user.username ?? userInfo?.username,
+ profile_uid: userProfile?.uid ?? uid ?? userInfo?.profile_uid,
+ };
+};
+
+const removeAllFromSet = (originalSet: Set, values: string[]) => {
+ const newSet = new Set(originalSet);
+ values.forEach((value) => newSet.delete(value));
+
+ return newSet;
+};
diff --git a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts
index 41d9904de01bd..fd7e08f222c9a 100644
--- a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts
+++ b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts
@@ -13,6 +13,7 @@ import { bulkGetCasesRoute } from './internal/bulk_get_cases';
import { suggestUserProfilesRoute } from './internal/suggest_user_profiles';
import type { CaseRoute } from './types';
import { bulkGetAttachmentsRoute } from './internal/bulk_get_attachments';
+import { getCaseUsersRoute } from './internal/get_case_users';
export const getInternalRoutes = (userProfileService: UserProfileService) =>
[
@@ -22,4 +23,5 @@ export const getInternalRoutes = (userProfileService: UserProfileService) =>
bulkGetCasesRoute,
getCaseUserActionStatsRoute,
bulkGetAttachmentsRoute,
+ getCaseUsersRoute,
] as CaseRoute[];
diff --git a/x-pack/plugins/cases/server/routes/api/internal/get_case_users.ts b/x-pack/plugins/cases/server/routes/api/internal/get_case_users.ts
new file mode 100644
index 0000000000000..56b031e548a54
--- /dev/null
+++ b/x-pack/plugins/cases/server/routes/api/internal/get_case_users.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { INTERNAL_CASE_USERS_URL } from '../../../../common/constants';
+import { createCaseError } from '../../../common/error';
+import { createCasesRoute } from '../create_cases_route';
+
+export const getCaseUsersRoute = createCasesRoute({
+ method: 'get',
+ path: INTERNAL_CASE_USERS_URL,
+ params: {
+ params: schema.object({
+ case_id: schema.string(),
+ }),
+ },
+ handler: async ({ context, request, response }) => {
+ try {
+ const casesContext = await context.cases;
+ const casesClient = await casesContext.getCasesClient();
+ const caseId = request.params.case_id;
+
+ return response.ok({
+ body: await casesClient.userActions.getUsers({ caseId }),
+ });
+ } catch (error) {
+ throw createCaseError({
+ message: `Failed to retrieve users in route case id: ${request.params.case_id}: ${error}`,
+ error,
+ });
+ }
+ },
+});
diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts
index 3060ca219ca42..067ecaaf8fe1e 100644
--- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts
+++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts
@@ -60,6 +60,12 @@ export const createCaseUserActionSavedObjectType = (
persistableStateAttachmentTypeId: { type: 'keyword' },
},
},
+ assignees: {
+ properties: {
+ // assignees.uid
+ uid: { type: 'keyword' },
+ },
+ },
},
},
owner: {
diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts
index ee65ce172c596..eb2bfd0fc8c47 100644
--- a/x-pack/plugins/cases/server/services/mocks.ts
+++ b/x-pack/plugins/cases/server/services/mocks.ts
@@ -125,6 +125,7 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => {
getUniqueConnectors: jest.fn(),
getUserActionIdsForCases: jest.fn(),
getCaseUserActionStats: jest.fn(),
+ getUsers: jest.fn(),
};
// the cast here is required because jest.Mocked tries to include private members and would throw an error
diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts
index 159cb40a5f1f8..252e3985a7483 100644
--- a/x-pack/plugins/cases/server/services/user_actions/index.ts
+++ b/x-pack/plugins/cases/server/services/user_actions/index.ts
@@ -18,6 +18,7 @@ import type {
CaseUserActionAttributesWithoutConnectorId,
CaseUserActionDeprecatedResponse,
CaseUserActionInjectedAttributes,
+ User,
} from '../../../common/api';
import { ActionTypes } from '../../../common/api';
import {
@@ -98,6 +99,29 @@ interface UserActionsStatsAggsResult {
};
}
+interface ParticipantsAggsResult {
+ participants: {
+ buckets: Array<{
+ key: string;
+ docs: {
+ hits: {
+ hits: SavedObjectsRawDoc[];
+ };
+ };
+ }>;
+ };
+ assignees: {
+ buckets: Array<{
+ key: string;
+ }>;
+ };
+}
+
+interface GetUsersResponse {
+ participants: Array<{ id: string; owner: string; user: User }>;
+ assignedAndUnassignedUsers: Set;
+}
+
export class CaseUserActionService {
private readonly _creator: UserActionPersister;
private readonly _finder: UserActionFinder;
@@ -715,4 +739,90 @@ export class CaseUserActionService {
},
};
}
+
+ public async getUsers({ caseId }: { caseId: string }): Promise {
+ const response = await this.context.unsecuredSavedObjectsClient.find<
+ CaseUserActionAttributesWithoutConnectorId,
+ ParticipantsAggsResult
+ >({
+ type: CASE_USER_ACTION_SAVED_OBJECT,
+ hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
+ page: 1,
+ perPage: 1,
+ sortField: defaultSortField,
+ aggs: CaseUserActionService.buildParticipantsAgg(),
+ });
+
+ const assignedAndUnassignedUsers: GetUsersResponse['assignedAndUnassignedUsers'] =
+ new Set();
+ const participants: GetUsersResponse['participants'] = [];
+ const participantsBuckets = response.aggregations?.participants.buckets ?? [];
+ const assigneesBuckets = response.aggregations?.assignees.buckets ?? [];
+
+ for (const bucket of participantsBuckets) {
+ const rawDoc = bucket.docs.hits.hits[0];
+ const user =
+ this.context.savedObjectsSerializer.rawToSavedObject(
+ rawDoc
+ );
+
+ /**
+ * We are interested only for the created_by
+ * and the owner. For that reason, there is no
+ * need to call transformToExternalModel which
+ * injects the references ids to the document.
+ */
+ participants.push({
+ id: user.id,
+ user: user.attributes.created_by,
+ owner: user.attributes.owner,
+ });
+ }
+
+ /**
+ * The users set includes any
+ * user that got assigned in the
+ * case even if they removed as
+ * assignee at some point in time.
+ */
+ for (const bucket of assigneesBuckets) {
+ assignedAndUnassignedUsers.add(bucket.key);
+ }
+
+ return { participants, assignedAndUnassignedUsers };
+ }
+
+ private static buildParticipantsAgg(): Record {
+ return {
+ participants: {
+ terms: {
+ field: `${CASE_USER_ACTION_SAVED_OBJECT}.attributes.created_by.username`,
+ size: MAX_DOCS_PER_PAGE,
+ order: { _key: 'asc' },
+ missing: 'Unknown',
+ },
+ aggregations: {
+ docs: {
+ top_hits: {
+ size: 1,
+ sort: [
+ {
+ [`${CASE_USER_ACTION_SAVED_OBJECT}.created_at`]: {
+ order: 'desc',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ assignees: {
+ terms: {
+ field: `${CASE_USER_ACTION_SAVED_OBJECT}.attributes.payload.assignees.uid`,
+ size: MAX_DOCS_PER_PAGE,
+ order: { _key: 'asc' },
+ },
+ },
+ };
+ }
}
diff --git a/x-pack/plugins/cloud_integrations/cloud_data_migration/public/application/components/app.tsx b/x-pack/plugins/cloud_integrations/cloud_data_migration/public/application/components/app.tsx
index d48cb672e05c3..2823a44c09a23 100755
--- a/x-pack/plugins/cloud_integrations/cloud_data_migration/public/application/components/app.tsx
+++ b/x-pack/plugins/cloud_integrations/cloud_data_migration/public/application/components/app.tsx
@@ -72,7 +72,7 @@ export const CloudDataMigrationApp = ({ http, breadcrumbService }: CloudDataMigr
@@ -86,7 +86,7 @@ export const CloudDataMigrationApp = ({ http, breadcrumbService }: CloudDataMigr
}
@@ -97,8 +97,8 @@ export const CloudDataMigrationApp = ({ http, breadcrumbService }: CloudDataMigr
label={
}
@@ -109,8 +109,8 @@ export const CloudDataMigrationApp = ({ http, breadcrumbService }: CloudDataMigr
label={
}
@@ -121,8 +121,8 @@ export const CloudDataMigrationApp = ({ http, breadcrumbService }: CloudDataMigr
label={
}
@@ -141,7 +141,7 @@ export const CloudDataMigrationApp = ({ http, breadcrumbService }: CloudDataMigr
>
diff --git a/x-pack/plugins/cloud_security_posture/README.md b/x-pack/plugins/cloud_security_posture/README.md
index 7a4646d08ba07..a655d292c39ee 100755
--- a/x-pack/plugins/cloud_security_posture/README.md
+++ b/x-pack/plugins/cloud_security_posture/README.md
@@ -49,3 +49,8 @@ yarn test:ftr --config x-pack/test/cloud_security_posture_functional/config.ts
> **Note**
> in development, run them separately with `ftr:runner` and `ftr:server`
+
+```bash
+yarn test:ftr:server --config x-pack/test/api_integration/config.ts
+yarn test:ftr:runner --include-tag=cloud_security_posture --config x-pack/test/api_integration/config.ts
+```
\ No newline at end of file
diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json
index 98d27ba9f0481..e7a136c1d5c83 100644
--- a/x-pack/plugins/data_visualizer/kibana.json
+++ b/x-pack/plugins/data_visualizer/kibana.json
@@ -32,7 +32,8 @@
"fieldFormats",
"uiActions",
"lens",
- "cloudChat"
+ "cloudChat",
+ "savedSearch"
],
"owner": {
"name": "Machine Learning UI",
diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
index fb782a02a08d4..5afa3869abd88 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
@@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { DataViewField } from '@kbn/data-views-plugin/public';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
-import { SupportedFieldType } from '../../../../common/types';
+import type { SupportedFieldType } from '../../../../common/types';
import { SUPPORTED_FIELD_TYPES } from '../../../../common/constants';
export const getJobTypeLabel = (type: string) => {
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
index 9f969fac23a38..ea72215a89598 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
@@ -36,6 +36,7 @@ import {
} from '@kbn/ml-date-picker';
import { useStorage } from '@kbn/ml-local-storage';
+import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme';
import {
DV_FROZEN_TIER_PREFERENCE,
@@ -57,7 +58,7 @@ import {
DataVisualizerIndexBasedPageUrlState,
} from '../../types/index_data_visualizer_state';
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../types/combined_query';
-import { SupportedFieldType, SavedSearchSavedObject } from '../../../../../common/types';
+import type { SupportedFieldType } from '../../../../../common/types';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { FieldCountPanel } from '../../../common/components/field_count_panel';
import { DocumentCountContent } from '../../../common/components/document_count_content';
@@ -129,7 +130,7 @@ export const getDefaultDataVisualizerListState = (
export interface IndexDataVisualizerViewProps {
currentDataView: DataView;
- currentSavedSearch: SavedSearchSavedObject | null;
+ currentSavedSearch: SavedSearch | null;
currentSessionId?: string;
getAdditionalLinks?: GetAdditionalLinks;
}
@@ -178,12 +179,6 @@ export const IndexDataVisualizerView: FC = (dataVi
const { currentDataView, currentSessionId, getAdditionalLinks } = dataVisualizerProps;
- useEffect(() => {
- if (dataVisualizerProps?.currentSavedSearch !== undefined) {
- setCurrentSavedSearch(dataVisualizerProps?.currentSavedSearch);
- }
- }, [dataVisualizerProps?.currentSavedSearch]);
-
useEffect(() => {
if (!currentDataView.isTimeBased()) {
toasts.addWarning({
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
index ac93003d1817e..9aeab958683e7 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
@@ -21,7 +21,7 @@ import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { isDefined } from '@kbn/ml-is-defined';
import { DataVisualizerFieldNamesFilter } from './field_name_filter';
import { DataVisualizerFieldTypeFilter } from './field_type_filter';
-import { SupportedFieldType } from '../../../../../common/types';
+import type { SupportedFieldType } from '../../../../../common/types';
import { SearchQueryLanguage } from '../../types/combined_query';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { createMergedEsQuery } from '../../utils/saved_search_utils';
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx
index 6534ace2343d8..d9cd0dfbaccfd 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx
@@ -27,8 +27,8 @@ import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-pl
import type { Query } from '@kbn/es-query';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
-import { SavedSearch } from '@kbn/discover-plugin/public';
-import { SamplingOption } from '../../../../../common/types/field_stats';
+import type { SavedSearch } from '@kbn/discover-plugin/public';
+import type { SamplingOption } from '../../../../../common/types/field_stats';
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
import { EmbeddableLoading } from './embeddable_loading_fallback';
import { DataVisualizerStartDependencies } from '../../../../plugin';
@@ -38,7 +38,7 @@ import {
} from '../../../common/components/stats_table';
import { FieldVisConfig } from '../../../common/components/stats_table/types';
import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view';
-import type { DataVisualizerTableState, SavedSearchSavedObject } from '../../../../../common/types';
+import type { DataVisualizerTableState } from '../../../../../common/types';
import type { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
@@ -46,7 +46,7 @@ import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_
export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies];
export interface DataVisualizerGridInput {
dataView: DataView;
- savedSearch?: SavedSearch | SavedSearchSavedObject | null;
+ savedSearch?: SavedSearch | null;
query?: Query;
visibleFieldNames?: string[];
filters?: Filter[];
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
index 858df7c6366b7..66902ed0fe190 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
@@ -98,7 +98,7 @@ export const useDataVisualizerGridData = (
);
/** Prepare required params to pass to search strategy **/
- const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
+ const { searchQueryLanguage, searchString, searchQuery, queryOrAggregateQuery } = useMemo(() => {
const filterManager = data.query.filterManager;
const searchData = getEsQueryFromSavedSearch({
dataView: currentDataView,
@@ -123,6 +123,7 @@ export const useDataVisualizerGridData = (
};
} else {
return {
+ queryOrAggregateQuery: searchData.queryOrAggregateQuery,
searchQuery: searchData.searchQuery,
searchString: searchData.searchString,
searchQueryLanguage: searchData.queryLanguage,
@@ -563,6 +564,7 @@ export const useDataVisualizerGridData = (
progress: combinedProgress,
overallStatsProgress,
configs,
+ queryOrAggregateQuery,
searchQueryLanguage,
searchString,
searchQuery,
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
index f7c70625f0e65..7633d63286f8e 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
@@ -11,7 +11,6 @@ import { useHistory, useLocation } from 'react-router-dom';
import { parse, stringify } from 'query-string';
import { isEqual } from 'lodash';
import { encode } from '@kbn/rison';
-import { SimpleSavedObject } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import {
@@ -33,14 +32,15 @@ import {
type Dictionary,
type SetUrlState,
} from '@kbn/ml-url-state';
+import { getSavedSearch, type SavedSearch } from '@kbn/saved-search-plugin/public';
import { getCoreStart, getPluginsStart } from '../../kibana_services';
import {
- IndexDataVisualizerViewProps,
+ type IndexDataVisualizerViewProps,
IndexDataVisualizerView,
} from './components/index_data_visualizer_view';
import { useDataVisualizerKibana } from '../kibana_context';
-import { GetAdditionalLinks } from '../common/components/results_links';
-import { DATA_VISUALIZER_APP_LOCATOR, IndexDataVisualizerLocatorParams } from './locator';
+import type { GetAdditionalLinks } from '../common/components/results_links';
+import { DATA_VISUALIZER_APP_LOCATOR, type IndexDataVisualizerLocatorParams } from './locator';
import { DATA_VISUALIZER_INDEX_VIEWER } from './constants/index_data_visualizer_viewer';
import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants';
import { DV_STORAGE_KEYS } from './types/storage';
@@ -100,9 +100,7 @@ export const DataVisualizerStateContextProvider: FC(undefined);
- const [currentSavedSearch, setCurrentSavedSearch] = useState | null>(
- null
- );
+ const [currentSavedSearch, setCurrentSavedSearch] = useState(null);
const [currentSessionId, setCurrentSessionId] = useState(undefined);
@@ -161,21 +159,21 @@ export const DataVisualizerStateContextProvider: FC ref.type === 'index-pattern')?.id;
- if (dataViewId !== undefined && savedSearch) {
- try {
- const dataView = await dataViews.get(dataViewId);
- setCurrentSavedSearch(savedSearch);
- setCurrentDataView(dataView);
- } catch (e) {
- toasts.addError(e, {
- title: i18n.translate('xpack.dataVisualizer.index.dataViewErrorMessage', {
- defaultMessage: 'Error finding data view',
- }),
- });
- }
+ const savedSearch = await getSavedSearch(savedSearchId, {
+ search,
+ savedObjectsClient,
+ });
+ const dataView = savedSearch.searchSource.getField('index');
+
+ if (!dataView) {
+ toasts.addDanger({
+ title: i18n.translate('xpack.dataVisualizer.index.dataViewErrorMessage', {
+ defaultMessage: 'Error finding data view',
+ }),
+ });
}
+ setCurrentSavedSearch(savedSearch);
+ setCurrentDataView(dataView);
} catch (e) {
toasts.addError(e, {
title: i18n.translate('xpack.dataVisualizer.index.savedSearchErrorMessage', {
@@ -192,7 +190,7 @@ export const DataVisualizerStateContextProvider: FC 49', language: 'lucene' } as Query,
+ filter: [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ params: [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ field: 'airline',
+ index: 'cb8808e0-9bfb-11ed-bb38-2b1bd55401e7',
+ key: 'airline',
+ negate: false,
+ params: {
+ query: 'ACA',
+ },
+ type: 'phrase',
+ },
+ query: {
+ match_phrase: {
+ airline: 'ACA',
+ },
+ },
+ },
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ field: 'airline',
+ index: 'cb8808e0-9bfb-11ed-bb38-2b1bd55401e7',
+ key: 'airline',
+ negate: false,
+ params: {
+ query: 'FFT',
+ },
+ type: 'phrase',
+ },
+ query: {
+ match_phrase: {
+ airline: 'FFT',
+ },
+ },
+ },
+ ] as FilterMetaParams,
+ // @ts-expect-error SavedSearch needs to be updated with CombinedFilterMeta
+ relation: 'OR',
+ type: 'combined',
+ index: 'cb8808e0-9bfb-11ed-bb38-2b1bd55401e7',
+ },
+ query: {},
+ $state: {
+ store: FilterStateStore.APP_STATE,
+ },
+ },
+ ],
+ }),
+} as unknown as SavedSearch;
+
// @ts-expect-error We don't need the full object here
const luceneSavedSearchObj: SavedSearchSavedObject = {
attributes: {
@@ -75,12 +145,27 @@ const kqlSavedSearch: SavedSearch = {
title: 'farequote_filter_and_kuery',
description: '',
columns: ['_source'],
- // @ts-expect-error this isn't a valid SavedSearch object... but does anyone care?
- kibanaSavedObjectMeta: {
- searchSourceJSON:
- '{"highlightAll":true,"version":true,"query":{"query":"responsetime > 49","language":"kuery"},"filter":[{"meta":{"index":"90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"airline","value":"ASA","params":{"query":"ASA","type":"phrase"}},"query":{"match":{"airline":{"query":"ASA","type":"phrase"}}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
- },
-};
+ searchSource: createSearchSourceMock({
+ index: mockDataView,
+ query: { query: 'responsetime > 49', language: 'kuery' } as Query,
+ filter: [
+ {
+ meta: {
+ index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
+ negate: false,
+ disabled: false,
+ alias: null,
+ type: 'phrase',
+ key: 'airline',
+ value: 'ASA',
+ params: { query: 'ASA', type: 'phrase' },
+ },
+ query: { match: { airline: { query: 'ASA', type: 'phrase' } } },
+ $state: { store: FilterStateStore.APP_STATE },
+ },
+ ],
+ }),
+} as unknown as SavedSearch;
describe('getQueryFromSavedSearchObject()', () => {
it('should return parsed searchSourceJSON with query and filter', () => {
@@ -107,26 +192,24 @@ describe('getQueryFromSavedSearchObject()', () => {
version: true,
});
expect(getQueryFromSavedSearchObject(kqlSavedSearch)).toEqual({
+ query: { query: 'responsetime > 49', language: 'kuery' },
+ index: 'test-mock-data-view',
filter: [
{
- $state: { store: 'appState' },
meta: {
- alias: null,
- disabled: false,
index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
- key: 'airline',
negate: false,
- params: { query: 'ASA', type: 'phrase' },
+ disabled: false,
+ alias: null,
type: 'phrase',
+ key: 'airline',
value: 'ASA',
+ params: { query: 'ASA', type: 'phrase' },
},
query: { match: { airline: { query: 'ASA', type: 'phrase' } } },
+ $state: { store: 'appState' },
},
],
- highlightAll: true,
- indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
- query: { language: 'kuery', query: 'responsetime > 49' },
- version: true,
});
});
it('should return undefined if invalid searchSourceJSON', () => {
@@ -222,27 +305,55 @@ describe('getEsQueryFromSavedSearch()', () => {
expect(
getEsQueryFromSavedSearch({
dataView: mockDataView,
- savedSearch: luceneSavedSearchObj,
+ savedSearch: luceneSavedSearch,
uiSettings: mockUiSettings,
})
).toEqual({
queryLanguage: 'lucene',
+ queryOrAggregateQuery: {
+ language: 'lucene',
+ query: 'responsetime > 49',
+ },
searchQuery: {
bool: {
- filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
- must: [{ query_string: { query: 'responsetime:>50' } }],
+ filter: [
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ must: [],
+ filter: [{ match_phrase: { airline: 'ACA' } }],
+ should: [],
+ must_not: [],
+ },
+ },
+ {
+ bool: {
+ must: [],
+ filter: [{ match_phrase: { airline: 'FFT' } }],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ must: [{ query_string: { query: 'responsetime > 49' } }],
must_not: [],
should: [],
},
},
- searchString: 'responsetime:>50',
+ searchString: 'responsetime > 49',
});
});
it('should override original saved search with the provided query ', () => {
expect(
getEsQueryFromSavedSearch({
dataView: mockDataView,
- savedSearch: luceneSavedSearchObj,
+ savedSearch: luceneSavedSearch,
uiSettings: mockUiSettings,
query: {
query: 'responsetime:>100',
@@ -251,9 +362,34 @@ describe('getEsQueryFromSavedSearch()', () => {
})
).toEqual({
queryLanguage: 'lucene',
+ queryOrAggregateQuery: { language: 'lucene', query: 'responsetime:>100' },
searchQuery: {
bool: {
- filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
+ filter: [
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ must: [],
+ filter: [{ match_phrase: { airline: 'ACA' } }],
+ should: [],
+ must_not: [],
+ },
+ },
+ {
+ bool: {
+ must: [],
+ filter: [{ match_phrase: { airline: 'FFT' } }],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
must: [{ query_string: { query: 'responsetime:>100' } }],
must_not: [],
should: [],
@@ -267,7 +403,7 @@ describe('getEsQueryFromSavedSearch()', () => {
expect(
getEsQueryFromSavedSearch({
dataView: mockDataView,
- savedSearch: luceneSavedSearchObj,
+ savedSearch: luceneSavedSearch,
uiSettings: mockUiSettings,
query: {
query: 'responsetime:>100',
@@ -299,6 +435,7 @@ describe('getEsQueryFromSavedSearch()', () => {
})
).toEqual({
queryLanguage: 'lucene',
+ queryOrAggregateQuery: { language: 'lucene', query: 'responsetime:>100' },
searchQuery: {
bool: {
filter: [],
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
index a4678c5ec5dec..c3473c170b370 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
@@ -16,13 +16,13 @@ import {
buildEsQuery,
Query,
Filter,
+ AggregateQuery,
} from '@kbn/es-query';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import { SearchSource } from '@kbn/data-plugin/common';
import { DataView } from '@kbn/data-views-plugin/public';
-import { SavedSearch } from '@kbn/discover-plugin/public';
-import { getEsQueryConfig } from '@kbn/data-plugin/common';
-import { FilterManager } from '@kbn/data-plugin/public';
+import type { SavedSearch } from '@kbn/saved-search-plugin/public';
+import { getEsQueryConfig, isQuery, SearchSource } from '@kbn/data-plugin/common';
+import { FilterManager, mapAndFlattenFilters } from '@kbn/data-plugin/public';
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query';
import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types';
@@ -45,10 +45,12 @@ export function getDefaultQuery() {
* from a saved search or saved search object
*/
export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject | SavedSearch) {
- const search = isSavedSearchSavedObject(savedSearch)
- ? savedSearch?.attributes?.kibanaSavedObjectMeta
- : // @ts-ignore
- savedSearch?.kibanaSavedObjectMeta;
+ if (!isSavedSearchSavedObject(savedSearch)) {
+ return savedSearch.searchSource.getSerializedFields();
+ }
+ const search =
+ savedSearch?.attributes?.kibanaSavedObjectMeta ?? // @ts-ignore
+ savedSearch?.kibanaSavedObjectMeta;
const parsed =
typeof search?.searchSourceJSON === 'string'
@@ -75,14 +77,14 @@ export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObjec
* Should also form a valid query if only the query or filters is provided
*/
export function createMergedEsQuery(
- query?: Query,
+ query?: Query | AggregateQuery | undefined,
filters?: Filter[],
dataView?: DataView,
uiSettings?: IUiSettingsClient
) {
let combinedQuery: QueryDslQueryContainer = getDefaultQuery();
- if (query && query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
+ if (isQuery(query) && query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
const ast = fromKueryExpression(query.query);
if (query.query !== '') {
combinedQuery = toElasticsearchQuery(ast, dataView);
@@ -114,6 +116,14 @@ export function createMergedEsQuery(
return combinedQuery;
}
+function getSavedSearchSource(savedSearch: SavedSearch) {
+ return savedSearch &&
+ 'searchSource' in savedSearch &&
+ savedSearch?.searchSource instanceof SearchSource
+ ? savedSearch.searchSource
+ : undefined;
+}
+
/**
* Extract query data from the saved search object
* with overrides from the provided query data and/or filters
@@ -128,7 +138,7 @@ export function getEsQueryFromSavedSearch({
}: {
dataView: DataView;
uiSettings: IUiSettingsClient;
- savedSearch: SavedSearchSavedObject | SavedSearch | null | undefined;
+ savedSearch: SavedSearch | null | undefined;
query?: Query;
filters?: Filter[];
filterManager?: FilterManager;
@@ -138,17 +148,13 @@ export function getEsQueryFromSavedSearch({
const userQuery = query;
const userFilters = filters;
+ const savedSearchSource = getSavedSearchSource(savedSearch);
+
// If saved search has a search source with nested parent
// e.g. a search coming from Dashboard saved search embeddable
// which already combines both the saved search's original query/filters and the Dashboard's
// then no need to process any further
- if (
- savedSearch &&
- 'searchSource' in savedSearch &&
- savedSearch?.searchSource instanceof SearchSource &&
- savedSearch.searchSource.getParent() !== undefined &&
- userQuery
- ) {
+ if (savedSearchSource && savedSearchSource.getParent() !== undefined && userQuery) {
// Flattened query from search source may contain a clause that narrows the time range
// which might interfere with global time pickers so we need to remove
const savedQuery =
@@ -168,13 +174,9 @@ export function getEsQueryFromSavedSearch({
};
}
- // If saved search is an json object with the original query and filter
- // retrieve the parsed query and filter
- const savedSearchData = getQueryFromSavedSearchObject(savedSearch);
-
// If no saved search available, use user's query and filters
- if (!savedSearchData && userQuery) {
- if (filterManager && userFilters) filterManager.addFilters(userFilters, false);
+ if (!savedSearch && userQuery) {
+ if (filterManager && userFilters) filterManager.addFilters(userFilters);
const combinedQuery = createMergedEsQuery(
userQuery,
@@ -190,13 +192,14 @@ export function getEsQueryFromSavedSearch({
};
}
- // If saved search available, merge saved search with latest user query or filters
+ // If saved search available, merge saved search with the latest user query or filters
// which might differ from extracted saved search data
- if (savedSearchData) {
+ if (savedSearchSource) {
const globalFilters = filterManager?.getGlobalFilters();
- const currentQuery = userQuery ?? savedSearchData?.query;
- const currentFilters = userFilters ?? savedSearchData?.filter;
-
+ // FIXME: Add support for AggregateQuery type #150091
+ const currentQuery = userQuery ?? (savedSearchSource.getField('query') as Query);
+ const currentFilters =
+ userFilters ?? mapAndFlattenFilters(savedSearchSource.getField('filter') as Filter[]);
if (filterManager) filterManager.setFilters(currentFilters);
if (globalFilters) filterManager?.addFilters(globalFilters);
@@ -209,8 +212,9 @@ export function getEsQueryFromSavedSearch({
return {
searchQuery: combinedQuery,
- searchString: currentQuery.query,
- queryLanguage: currentQuery.language as SearchQueryLanguage,
+ searchString: currentQuery?.query ?? '',
+ queryLanguage: (currentQuery?.language as SearchQueryLanguage) ?? 'kuery',
+ queryOrAggregateQuery: currentQuery,
};
}
}
diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json
index fd61172e31587..7472c3910f7fe 100644
--- a/x-pack/plugins/data_visualizer/tsconfig.json
+++ b/x-pack/plugins/data_visualizer/tsconfig.json
@@ -56,6 +56,7 @@
"@kbn/ml-date-picker",
"@kbn/ml-is-defined",
"@kbn/ml-query-utils",
+ "@kbn/saved-search-plugin",
],
"exclude": [
"target/**/*",
diff --git a/x-pack/plugins/enterprise_search/common/types/connectors.ts b/x-pack/plugins/enterprise_search/common/types/connectors.ts
index 2913423b46285..78f3f9e6a51e4 100644
--- a/x-pack/plugins/enterprise_search/common/types/connectors.ts
+++ b/x-pack/plugins/enterprise_search/common/types/connectors.ts
@@ -10,7 +10,9 @@ export interface KeyValuePair {
value: string | null;
}
-export type ConnectorConfiguration = Record;
+export type ConnectorConfiguration = Record & {
+ extract_full_html?: { label: string; value: boolean };
+};
export interface ConnectorScheduling {
enabled: boolean;
@@ -27,10 +29,6 @@ export interface CustomScheduling {
export type ConnectorCustomScheduling = Record;
-export interface ConnectorPreferences extends Record {
- extract_full_html?: boolean | null;
-}
-
export enum ConnectorStatus {
CREATED = 'created',
NEEDS_CONFIGURATION = 'needs_configuration',
@@ -154,7 +152,6 @@ export interface Connector {
last_synced: string | null;
name: string;
pipeline?: IngestPipelineParams | null;
- preferences: ConnectorPreferences;
scheduling: {
enabled: boolean;
interval: string; // crontab syntax
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts
index c9ccc1c0e7e65..955cf219a8019 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts
@@ -105,7 +105,6 @@ export const indices: ElasticsearchIndexWithIngestion[] = [
last_sync_status: SyncStatus.COMPLETED,
last_synced: null,
name: 'connector',
- preferences: { extract_full_html: false },
scheduling: {
enabled: false,
interval: '',
@@ -201,7 +200,6 @@ export const indices: ElasticsearchIndexWithIngestion[] = [
last_sync_status: SyncStatus.COMPLETED,
last_synced: null,
name: 'crawler',
- preferences: { extract_full_html: false },
scheduling: {
enabled: false,
interval: '',
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts
index 05211e48bf134..78ef10664abcb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts
@@ -115,7 +115,6 @@ export const connectorIndex: ConnectorViewIndex = {
last_sync_status: SyncStatus.COMPLETED,
last_synced: null,
name: 'connector',
- preferences: { extract_full_html: false },
scheduling: {
enabled: false,
interval: '',
@@ -215,7 +214,6 @@ export const crawlerIndex: CrawlerViewIndex = {
last_sync_status: SyncStatus.COMPLETED,
last_synced: null,
name: 'crawler',
- preferences: { extract_full_html: false },
scheduling: {
enabled: false,
interval: '',
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/update_html_extraction_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/update_html_extraction_api_logic.test.ts
new file mode 100644
index 0000000000000..163a29e07f42e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/update_html_extraction_api_logic.test.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 { mockHttpValues } from '../../../__mocks__/kea_logic';
+
+import { updateHtmlExtraction } from './update_html_extraction_api_logic';
+
+describe('UpdateHtmlExtractionApiLogic', () => {
+ const { http } = mockHttpValues;
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ describe('updateHtmlExtraction', () => {
+ it('calls correct api', async () => {
+ const indexName = 'elastic-co-crawler';
+
+ http.get.mockReturnValue(Promise.resolve());
+
+ const result = updateHtmlExtraction({ htmlExtraction: true, indexName });
+
+ expect(http.put).toHaveBeenCalledWith(
+ `/internal/enterprise_search/indices/${indexName}/crawler/html_extraction`,
+ {
+ body: JSON.stringify({
+ extract_full_html: true,
+ }),
+ }
+ );
+ await expect(result).resolves.toEqual({ htmlExtraction: true });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/update_html_extraction_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/update_html_extraction_api_logic.ts
new file mode 100644
index 0000000000000..e6ddcc5f82071
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/update_html_extraction_api_logic.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { Actions } from '../../../shared/api_logic/create_api_logic';
+
+import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
+import { HttpLogic } from '../../../shared/http';
+
+export interface UpdateHtmlExtractionArgs {
+ htmlExtraction: boolean;
+ indexName: string;
+}
+
+export interface UpdateHtmlExtractionResponse {
+ htmlExtraction: boolean;
+}
+
+export const updateHtmlExtraction = async ({
+ htmlExtraction,
+ indexName,
+}: UpdateHtmlExtractionArgs) => {
+ const route = `/internal/enterprise_search/indices/${indexName}/crawler/html_extraction`;
+
+ const params = { extract_full_html: htmlExtraction };
+
+ await HttpLogic.values.http.put(route, {
+ body: JSON.stringify(params),
+ });
+ return { htmlExtraction };
+};
+
+export const UpdateHtmlExtractionApiLogic = createApiLogic(
+ ['update_html_extraction_api_logic'],
+ updateHtmlExtraction
+);
+
+export type UpdateHtmlExtractionActions = Actions<
+ UpdateHtmlExtractionArgs,
+ UpdateHtmlExtractionResponse
+>;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/add_indices_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/add_indices_flyout.tsx
index 852e3698b968c..22ab72ba3a405 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/add_indices_flyout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/add_indices_flyout.tsx
@@ -99,7 +99,12 @@ export const AddIndicesFlyout: React.FC = ({ onClose }) =
-
+
{i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.addIndicesFlyout.submitButton',
{ defaultMessage: 'Add selected' }
@@ -107,7 +112,11 @@ export const AddIndicesFlyout: React.FC = ({ onClose }) =
-
+
{i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.addIndicesFlyout.cancelButton',
{ defaultMessage: 'Cancel' }
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx
index 0bcdde5e47647..da664b3d97490 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx
@@ -28,6 +28,7 @@ import { indexHealthToHealthColor } from '../../../shared/constants/health_color
import { generateEncodedPath } from '../../../shared/encode_path_params';
import { KibanaLogic } from '../../../shared/kibana';
import { EuiLinkTo } from '../../../shared/react_router_helpers';
+import { TelemetryLogic } from '../../../shared/telemetry/telemetry_logic';
import { SEARCH_INDEX_PATH, EngineViewTabs } from '../../routes';
import { IngestionMethod } from '../../types';
@@ -40,6 +41,7 @@ import { EngineIndicesLogic } from './engine_indices_logic';
import { EngineViewHeaderActions } from './engine_view_header_actions';
export const EngineIndices: React.FC = () => {
+ const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic);
const { engineData, engineName, isLoadingEngine, addIndicesFlyoutOpen } =
useValues(EngineIndicesLogic);
const { removeIndexFromEngine, openAddIndicesFlyout, closeAddIndicesFlyout } =
@@ -157,7 +159,13 @@ export const EngineIndices: React.FC = () => {
},
}
),
- onClick: (index) => setConfirmRemoveIndex(index.name),
+ onClick: (index) => {
+ setConfirmRemoveIndex(index.name);
+ sendEnterpriseSearchTelemetry({
+ action: 'clicked',
+ metric: 'entSearchContent-engines-indices-removeIndex',
+ });
+ },
type: 'icon',
},
],
@@ -180,6 +188,7 @@ export const EngineIndices: React.FC = () => {
{
onConfirm={() => {
removeIndexFromEngine(removeIndexConfirm);
setConfirmRemoveIndex(null);
+ sendEnterpriseSearchTelemetry({
+ action: 'clicked',
+ metric: 'entSearchContent-engines-indices-removeIndexConfirm',
+ });
}}
title={i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title',
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_header_actions.tsx
index ba563a9c23dbc..bd48ee8d6d294 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_header_actions.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_header_actions.tsx
@@ -13,12 +13,15 @@ import { EuiPopover, EuiButtonIcon, EuiText, EuiContextMenu, EuiIcon } from '@el
import { i18n } from '@kbn/i18n';
+import { TelemetryLogic } from '../../../shared/telemetry/telemetry_logic';
+
import { EngineViewLogic } from './engine_view_logic';
export const EngineViewHeaderActions: React.FC = () => {
const { engineData } = useValues(EngineViewLogic);
const { openDeleteEngineModal } = useActions(EngineViewLogic);
+ const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic);
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
const toggleActionsPopover = () => setIsActionsPopoverOpen((isPopoverOpen) => !isPopoverOpen);
@@ -66,6 +69,10 @@ export const EngineViewHeaderActions: React.FC = () => {
onClick: () => {
if (engineData) {
openDeleteEngineModal();
+ sendEnterpriseSearchTelemetry({
+ action: 'clicked',
+ metric: 'entSearchContent-engines-engineView-deleteEngine',
+ });
}
},
size: 's',
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/header_docs_action.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/header_docs_action.tsx
index 93544cc2e9bbd..e20fbc81689a4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/header_docs_action.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/header_docs_action.tsx
@@ -15,8 +15,9 @@ export const EngineHeaderDocsAction: React.FC = () => (
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/tables/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/tables/engines_table.tsx
index b3c5c46b38128..017564343f11e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/tables/engines_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/tables/engines_table.tsx
@@ -7,7 +7,7 @@
import React from 'react';
-import { useValues } from 'kea';
+import { useValues, useActions } from 'kea';
import {
CriteriaWithPagination,
@@ -29,6 +29,7 @@ import { FormattedDateTime } from '../../../../../shared/formatted_date_time';
import { KibanaLogic } from '../../../../../shared/kibana';
import { pageToPagination } from '../../../../../shared/pagination/page_to_pagination';
import { EuiLinkTo } from '../../../../../shared/react_router_helpers';
+import { TelemetryLogic } from '../../../../../shared/telemetry/telemetry_logic';
import { ENGINE_PATH } from '../../../../routes';
@@ -50,6 +51,7 @@ export const EnginesListTable: React.FC = ({
viewEngineIndices,
}) => {
const { navigateToUrl } = useValues(KibanaLogic);
+ const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic);
const columns: Array> = [
{
field: 'name',
@@ -93,6 +95,7 @@ export const EnginesListTable: React.FC = ({
size="s"
className="engineListTableFlyoutButton"
data-test-subj="engineListTableIndicesFlyoutButton"
+ data-telemetry-id="entSearchContent-engines-table-viewEngineIndices"
onClick={() => viewEngineIndices(engine.name)}
>
= ({
),
onClick: (engine) => {
onDelete(engine);
+ sendEnterpriseSearchTelemetry({
+ action: 'clicked',
+ metric: 'entSearchContent-engines-table-deleteEngine',
+ });
},
},
],
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/create_engine_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/create_engine_flyout.tsx
index 06a2509edad53..ae7f7423be7ce 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/create_engine_flyout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/create_engine_flyout.tsx
@@ -36,7 +36,7 @@ import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/ind
import { isNotNullish } from '../../../../../common/utils/is_not_nullish';
import { CANCEL_BUTTON_LABEL } from '../../../shared/constants';
-
+import { docLinks } from '../../../shared/doc_links';
import { getErrorsFromHttpResponse } from '../../../shared/flash_messages/handle_api_errors';
import { indexToOption, IndicesSelectComboBox } from './components/indices_select_combobox';
@@ -85,7 +85,7 @@ export const CreateEngineFlyout = ({ onClose }: CreateEngineFlyoutProps) => {
values={{
enginesDocsLink: (
= ({ engineName, onClose }) => {
const { deleteEngine } = useActions(EnginesListLogic);
+ const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic);
const { isDeleteLoading } = useValues(EnginesListLogic);
return (
= ({ engineName
onCancel={onClose}
onConfirm={() => {
deleteEngine({ engineName });
+ sendEnterpriseSearchTelemetry({
+ action: 'clicked',
+ metric: 'entSearchContent-engines-engineView-deleteEngineConfirm',
+ });
}}
cancelButtonText={CANCEL_BUTTON_LABEL}
confirmButtonText={i18n.translate(
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx
index 76e343e234ff3..b927e499ff06c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx
@@ -16,6 +16,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
import { INPUT_THROTTLE_DELAY_MS } from '../../../shared/constants/timers';
+import { docLinks } from '../../../shared/doc_links';
import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template';
@@ -102,12 +103,10 @@ export const EnginesList: React.FC = () => {
documentationUrl: (
- {' '}
- {/* TODO: navigate to documentation url */}{' '}
{i18n.translate('xpack.enterpriseSearch.content.engines.documentation', {
defaultMessage: 'explore our Engines documentation',
})}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_configuration/crawler_configuration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_configuration/crawler_configuration.tsx
new file mode 100644
index 0000000000000..4741525a85b72
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_configuration/crawler_configuration.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { useActions, useValues } from 'kea';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiSpacer,
+ EuiSplitPanel,
+ EuiSwitch,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { Status } from '../../../../../../../common/types/api';
+
+import { IndexViewLogic } from '../../index_view_logic';
+
+import { CrawlerConfigurationLogic } from './crawler_configuration_logic';
+
+export const CrawlerConfiguration: React.FC = () => {
+ const { htmlExtraction } = useValues(IndexViewLogic);
+ const { status } = useValues(CrawlerConfigurationLogic);
+ const { updateHtmlExtraction } = useActions(CrawlerConfigurationLogic);
+ return (
+ <>
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.crawler.crawlerConfiguration.extractHTML.title',
+ { defaultMessage: 'Store full HTML' }
+ )}
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.crawler.crawlerConfiguration.extractHTML.addExtraFieldDescription',
+ {
+ defaultMessage:
+ 'Add an extra field in all documents with the value of the full HTML of the page being crawled.',
+ }
+ )}
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.crawler.crawlerConfiguration.extractHTML.increasedSizeWarning',
+ {
+ defaultMessage:
+ 'This may dramatically increase the index size if the site being crawled is large.',
+ }
+ )}
+
+
+
+
+
+
+ updateHtmlExtraction(event.target.checked)}
+ />
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.crawler.crawlerConfiguration.extractHTML.learnMoreLink',
+ {
+ defaultMessage: 'Learn more about storing full HTML.',
+ }
+ )}
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_configuration/crawler_configuration_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_configuration/crawler_configuration_logic.ts
new file mode 100644
index 0000000000000..9298ca3b32585
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_configuration/crawler_configuration_logic.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { kea, MakeLogicType } from 'kea';
+
+import { Status } from '../../../../../../../common/types/api';
+import { Connector } from '../../../../../../../common/types/connectors';
+
+import {
+ UpdateHtmlExtractionActions,
+ UpdateHtmlExtractionApiLogic,
+} from '../../../../api/crawler/update_html_extraction_api_logic';
+import { CachedFetchIndexApiLogicActions } from '../../../../api/index/cached_fetch_index_api_logic';
+import { isCrawlerIndex } from '../../../../utils/indices';
+import { IndexViewLogic } from '../../index_view_logic';
+
+interface CrawlerConfigurationLogicActions {
+ apiError: UpdateHtmlExtractionActions['apiError'];
+ apiSuccess: UpdateHtmlExtractionActions['apiSuccess'];
+ fetchIndex: () => void;
+ fetchIndexApiSuccess: CachedFetchIndexApiLogicActions['apiSuccess'];
+ htmlExtraction: boolean;
+ makeRequest: UpdateHtmlExtractionActions['makeRequest'];
+ updateHtmlExtraction(htmlExtraction: boolean): { htmlExtraction: boolean };
+}
+
+interface CrawlerConfigurationLogicValues {
+ connector: Connector | undefined;
+ indexName: string;
+ localHtmlExtraction: boolean | null;
+ status: Status;
+}
+
+export const CrawlerConfigurationLogic = kea<
+ MakeLogicType
+>({
+ actions: {
+ updateHtmlExtraction: (htmlExtraction) => ({ htmlExtraction }),
+ },
+ connect: {
+ actions: [
+ IndexViewLogic,
+ ['fetchIndex', 'fetchIndexApiSuccess'],
+ UpdateHtmlExtractionApiLogic,
+ ['apiSuccess', 'makeRequest'],
+ ],
+ values: [IndexViewLogic, ['connector', 'indexName'], UpdateHtmlExtractionApiLogic, ['status']],
+ },
+ listeners: ({ actions, values }) => ({
+ apiSuccess: () => {
+ actions.fetchIndex();
+ },
+ updateHtmlExtraction: ({ htmlExtraction }) => {
+ actions.makeRequest({ htmlExtraction, indexName: values.indexName });
+ },
+ }),
+ path: ['enterprise_search', 'search_index', 'crawler', 'configuration'],
+ reducers: {
+ localHtmlExtraction: [
+ null,
+ {
+ apiSuccess: (_, { htmlExtraction }) => htmlExtraction,
+ fetchIndexApiSuccess: (_, index) => {
+ if (isCrawlerIndex(index)) {
+ return index.connector.configuration.extract_full_html?.value ?? null;
+ }
+ return null;
+ },
+ },
+ ],
+ },
+ selectors: ({ selectors }) => ({
+ htmlExtraction: [
+ () => [selectors.connector, selectors.localHtmlExtraction],
+ (connector: Connector | null, localHtmlExtraction: boolean | null) =>
+ localHtmlExtraction !== null
+ ? localHtmlExtraction
+ : connector?.configuration.extract_full_html?.value ?? false,
+ ],
+ }),
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_view_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_view_logic.ts
index 26c1f823b479d..4ed5b7cef1b41 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_view_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_view_logic.ts
@@ -70,6 +70,7 @@ export interface IndexViewValues {
hasAdvancedFilteringFeature: boolean;
hasBasicFilteringFeature: boolean;
hasFilteringFeature: boolean;
+ htmlExtraction: boolean | undefined;
index: ElasticsearchViewIndex | undefined;
indexData: typeof CachedFetchIndexApiLogic.values.indexData;
indexName: string;
@@ -214,6 +215,11 @@ export const IndexViewLogic = kea [selectors.hasAdvancedFilteringFeature, selectors.hasBasicFilteringFeature],
(advancedFeature: boolean, basicFeature: boolean) => advancedFeature || basicFeature,
],
+ htmlExtraction: [
+ () => [selectors.connector],
+ (connector: Connector | undefined) =>
+ connector?.configuration.extract_full_html?.value ?? undefined,
+ ],
index: [
() => [selectors.indexData],
(data: IndexViewValues['indexData']) => (data ? indexToViewIndex(data) : undefined),
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx
index 5c5b691d42ce1..b09a0a9d8a96a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx
@@ -36,6 +36,7 @@ import { ConnectorSchedulingComponent } from './connector/connector_scheduling';
import { ConnectorSyncRules } from './connector/sync_rules/connector_rules';
import { AutomaticCrawlScheduler } from './crawler/automatic_crawl_scheduler/automatic_crawl_scheduler';
import { CrawlCustomSettingsFlyout } from './crawler/crawl_custom_settings_flyout/crawl_custom_settings_flyout';
+import { CrawlerConfiguration } from './crawler/crawler_configuration/crawler_configuration';
import { SearchIndexDomainManagement } from './crawler/domain_management/domain_management';
import { SearchIndexDocuments } from './documents';
import { SearchIndexIndexMappings } from './index_mappings';
@@ -56,6 +57,7 @@ export enum SearchIndexTabId {
SCHEDULING = 'scheduling',
// crawler indices
DOMAIN_MANAGEMENT = 'domain_management',
+ CRAWLER_CONFIGURATION = 'crawler_configuration',
}
export const SearchIndex: React.FC = () => {
@@ -164,6 +166,16 @@ export const SearchIndex: React.FC = () => {
defaultMessage: 'Manage Domains',
}),
},
+ {
+ content: ,
+ id: SearchIndexTabId.CRAWLER_CONFIGURATION,
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.content.searchIndex.crawlerConfigurationTabLabel',
+ {
+ defaultMessage: 'Configuration',
+ }
+ ),
+ },
{
content: ,
id: SearchIndexTabId.SCHEDULING,
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts
index 4185addb31445..57ff05050ebae 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts
@@ -32,7 +32,6 @@ export const ENGINE_TAB_PATH = `${ENGINE_PATH}/:tabId`;
export enum EngineViewTabs {
OVERVIEW = 'overview',
INDICES = 'indices',
- DOCUMENTS = 'documents',
SCHEMA = 'schema',
PREVIEW = 'preview',
API = 'api',
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
index cc1356fc0c140..073ffe6abca3f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
@@ -71,6 +71,7 @@ class DocLinks {
public elasticsearchMapping: string;
public elasticsearchSecureCluster: string;
public enterpriseSearchConfig: string;
+ public enterpriseSearchEngines: string;
public enterpriseSearchMailService: string;
public enterpriseSearchTroubleshootSetup: string;
public enterpriseSearchUsersAccess: string;
@@ -186,6 +187,7 @@ class DocLinks {
this.elasticsearchMapping = '';
this.elasticsearchSecureCluster = '';
this.enterpriseSearchConfig = '';
+ this.enterpriseSearchEngines = '';
this.enterpriseSearchMailService = '';
this.enterpriseSearchTroubleshootSetup = '';
this.enterpriseSearchUsersAccess = '';
@@ -302,6 +304,7 @@ class DocLinks {
this.elasticsearchMapping = docLinks.links.elasticsearch.mapping;
this.elasticsearchSecureCluster = docLinks.links.elasticsearch.secureCluster;
this.enterpriseSearchConfig = docLinks.links.enterpriseSearch.configuration;
+ this.enterpriseSearchEngines = docLinks.links.enterpriseSearch.engines;
this.enterpriseSearchMailService = docLinks.links.enterpriseSearch.mailService;
this.enterpriseSearchTroubleshootSetup = docLinks.links.enterpriseSearch.troubleshootSetup;
this.enterpriseSearchUsersAccess = docLinks.links.enterpriseSearch.usersAccess;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx
index 17ce1fce0512f..fac9d911be6c8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx
@@ -468,21 +468,16 @@ describe('useEnterpriseSearchEngineNav', () => {
id: 'enterpriseSearchEngineIndices',
name: 'Indices',
},
- {
- href: `/app/enterprise_search/content/engines/${engineName}/documents`,
- id: 'enterpriseSearchEngineDocuments',
- name: 'Documents',
- },
- {
- href: `/app/enterprise_search/content/engines/${engineName}/schema`,
- id: 'enterpriseSearchEngineSchema',
- name: 'Schema',
- },
- {
- href: `/app/enterprise_search/content/engines/${engineName}/preview`,
- id: 'enterpriseSearchEnginePreview',
- name: 'Preview',
- },
+ // {
+ // href: `/app/enterprise_search/content/engines/${engineName}/schema`,
+ // id: 'enterpriseSearchEngineSchema',
+ // name: 'Schema',
+ // },
+ // {
+ // href: `/app/enterprise_search/content/engines/${engineName}/preview`,
+ // id: 'enterpriseSearchEnginePreview',
+ // name: 'Preview',
+ // },
{
href: `/app/enterprise_search/content/engines/${engineName}/api`,
id: 'enterpriseSearchEngineAPI',
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx
index a6e550e7c58b2..3d81f5d9d7c34 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx
@@ -271,37 +271,27 @@ export const useEnterpriseSearchEngineNav = (engineName?: string, isEmptyState?:
to: `${enginePath}/${EngineViewTabs.INDICES}`,
}),
},
-
- {
- id: 'enterpriseSearchEngineDocuments',
- name: i18n.translate('xpack.enterpriseSearch.nav.engine.documentsTitle', {
- defaultMessage: 'Documents',
- }),
- ...generateNavLink({
- shouldNotCreateHref: true,
- to: `${enginePath}/${EngineViewTabs.DOCUMENTS}`,
- }),
- },
- {
- id: 'enterpriseSearchEngineSchema',
- name: i18n.translate('xpack.enterpriseSearch.nav.engine.schemaTitle', {
- defaultMessage: 'Schema',
- }),
- ...generateNavLink({
- shouldNotCreateHref: true,
- to: `${enginePath}/${EngineViewTabs.SCHEMA}`,
- }),
- },
- {
- id: 'enterpriseSearchEnginePreview',
- name: i18n.translate('xpack.enterpriseSearch.nav.engine.previewTitle', {
- defaultMessage: 'Preview',
- }),
- ...generateNavLink({
- shouldNotCreateHref: true,
- to: `${enginePath}/${EngineViewTabs.PREVIEW}`,
- }),
- },
+ // {
+ // id: 'enterpriseSearchEngineSchema',
+ // name: i18n.translate('xpack.enterpriseSearch.nav.engine.schemaTitle', {
+ // defaultMessage: 'Schema',
+ // }),
+ // ...generateNavLink({
+ // shouldNotCreateHref: true,
+ // to: `${enginePath}/${EngineViewTabs.SCHEMA}`,
+ // }),
+ // },
+ // Hidden until Preview page is available
+ // {
+ // id: 'enterpriseSearchEnginePreview',
+ // name: i18n.translate('xpack.enterpriseSearch.nav.engine.previewTitle', {
+ // defaultMessage: 'Preview',
+ // }),
+ // ...generateNavLink({
+ // shouldNotCreateHref: true,
+ // to: `${enginePath}/${EngineViewTabs.PREVIEW}`,
+ // }),
+ // },
{
id: 'enterpriseSearchEngineAPI',
name: i18n.translate('xpack.enterpriseSearch.nav.engine.apiTitle', {
diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts
index 8c8c16e9291f9..bee878338cdc6 100644
--- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts
+++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts
@@ -31,6 +31,7 @@ describe('Setup Indices', () => {
version: CONNECTORS_VERSION,
pipeline: defaultConnectorsPipelineMeta,
},
+ dynamic: false,
properties: {
api_key_id: {
type: 'keyword',
@@ -140,11 +141,6 @@ describe('Setup Indices', () => {
run_ml_inference: { type: 'boolean' },
},
},
- preferences: {
- properties: {
- extract_full_html: { type: 'boolean' },
- },
- },
scheduling: {
properties: {
enabled: { type: 'boolean' },
@@ -161,6 +157,7 @@ describe('Setup Indices', () => {
_meta: {
version: CONNECTORS_VERSION,
},
+ dynamic: false,
properties: {
cancelation_requested_at: { type: 'date' },
canceled_at: { type: 'date' },
diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts
index 488b0fc10930b..b70fe43303b26 100644
--- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts
+++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts
@@ -130,11 +130,6 @@ const connectorMappingsProperties: Record = {
run_ml_inference: { type: 'boolean' },
},
},
- preferences: {
- properties: {
- extract_full_html: { type: 'boolean' },
- },
- },
scheduling: {
properties: {
enabled: { type: 'boolean' },
@@ -174,6 +169,7 @@ const indices: IndexDefinition[] = [
pipeline: defaultConnectorsPipelineMeta,
version: 1,
},
+ dynamic: false,
properties: connectorMappingsProperties,
},
name: '.elastic-connectors-v1',
@@ -185,6 +181,7 @@ const indices: IndexDefinition[] = [
_meta: {
version: 1,
},
+ dynamic: false,
properties: {
cancelation_requested_at: { type: 'date' },
canceled_at: { type: 'date' },
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts
index c3b52f53857d6..e6584c0a8b205 100644
--- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts
@@ -154,7 +154,6 @@ describe('addConnector lib function', () => {
reduce_whitespace: true,
run_ml_inference: false,
},
- preferences: {},
scheduling: { enabled: false, interval: '0 0 0 * * ?' },
service_type: null,
status: ConnectorStatus.CREATED,
@@ -340,7 +339,6 @@ describe('addConnector lib function', () => {
reduce_whitespace: true,
run_ml_inference: false,
},
- preferences: {},
scheduling: { enabled: false, interval: '0 0 0 * * ?' },
service_type: null,
status: ConnectorStatus.CREATED,
@@ -448,7 +446,6 @@ describe('addConnector lib function', () => {
reduce_whitespace: true,
run_ml_inference: false,
},
- preferences: {},
scheduling: { enabled: false, interval: '0 0 0 * * ?' },
service_type: null,
status: ConnectorStatus.CREATED,
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts
index abe52caf453d2..507ec3fd0bb4f 100644
--- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts
@@ -164,7 +164,6 @@ export const addConnector = async (
run_ml_inference: connectorsPipelineMeta.default_run_ml_inference,
}
: null,
- preferences: {},
scheduling: { enabled: false, interval: '0 0 0 * * ?' },
service_type: input.service_type || null,
status: ConnectorStatus.CREATED,
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
index 2ea8a875bd3b4..25956b271245a 100644
--- a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
@@ -42,7 +42,6 @@ describe('addConnector lib function', () => {
last_sync_error: null,
last_sync_status: null,
last_synced: null,
- preferences: {},
scheduling: { enabled: true, interval: '1 2 3 4 5' },
service_type: null,
status: 'not connected',
@@ -68,7 +67,6 @@ describe('addConnector lib function', () => {
last_sync_error: null,
last_sync_status: null,
last_synced: null,
- preferences: {},
scheduling: { enabled: true, interval: '1 2 3 4 5' },
service_type: null,
status: 'not connected',
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts
index 89b936a41a889..2fa4c0954b245 100644
--- a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts
@@ -41,7 +41,6 @@ describe('addConnector lib function', () => {
last_sync_error: null,
last_sync_status: null,
last_synced: null,
- preferences: {},
scheduling: { enabled: false, interval: '* * * * *' },
service_type: null,
status: 'not connected',
@@ -70,7 +69,6 @@ describe('addConnector lib function', () => {
last_sync_error: null,
last_sync_status: null,
last_synced: null,
- preferences: {},
scheduling: { enabled: true, interval: '1 2 3 4 5' },
service_type: null,
status: 'not connected',
diff --git a/x-pack/plugins/enterprise_search/server/lib/crawler/put_html_extraction.test.ts b/x-pack/plugins/enterprise_search/server/lib/crawler/put_html_extraction.test.ts
new file mode 100644
index 0000000000000..d859d9639b2b6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/crawler/put_html_extraction.test.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IScopedClusterClient } from '@kbn/core/server';
+
+import { CONNECTORS_INDEX } from '../..';
+import { Connector } from '../../../common/types/connectors';
+
+import { updateHtmlExtraction } from './put_html_extraction';
+
+describe('updateHtmlExtraction lib function', () => {
+ const mockClient = {
+ asCurrentUser: {
+ update: jest.fn(),
+ },
+ asInternalUser: {},
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should update connector configuration', async () => {
+ mockClient.asCurrentUser.update.mockResolvedValue(true);
+ const mockConnector = {
+ configuration: { test: { label: 'haha', value: 'this' } },
+ id: 'connectorId',
+ };
+
+ await updateHtmlExtraction(
+ mockClient as unknown as IScopedClusterClient,
+ true,
+ mockConnector as any as Connector
+ );
+ expect(mockClient.asCurrentUser.update).toHaveBeenCalledWith({
+ doc: {
+ configuration: {
+ ...mockConnector.configuration,
+ extract_full_html: { label: 'Extract full HTML', value: true },
+ },
+ },
+ id: 'connectorId',
+ index: CONNECTORS_INDEX,
+ refresh: 'wait_for',
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/lib/crawler/put_html_extraction.ts b/x-pack/plugins/enterprise_search/server/lib/crawler/put_html_extraction.ts
new file mode 100644
index 0000000000000..bf7c18a575853
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/crawler/put_html_extraction.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
+
+import { CONNECTORS_INDEX } from '../..';
+import { Connector } from '../../../common/types/connectors';
+
+export async function updateHtmlExtraction(
+ client: IScopedClusterClient,
+ htmlExtraction: boolean,
+ connector: Connector
+) {
+ return await client.asCurrentUser.update({
+ doc: {
+ configuration: {
+ ...connector.configuration,
+ extract_full_html: {
+ label: 'Extract full HTML',
+ value: htmlExtraction,
+ },
+ },
+ },
+ id: connector.id,
+ index: CONNECTORS_INDEX,
+ refresh: 'wait_for',
+ });
+}
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.ts
index c3b034f0b6ce7..08cea961709fc 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.ts
@@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
+
import { i18n } from '@kbn/i18n';
import { ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE } from '../../../../common/constants';
@@ -15,6 +16,7 @@ import { addConnector } from '../../../lib/connectors/add_connector';
import { deleteConnectorById } from '../../../lib/connectors/delete_connector';
import { fetchConnectorByIndexName } from '../../../lib/connectors/fetch_connectors';
import { fetchCrawlerByIndexName } from '../../../lib/crawler/fetch_crawlers';
+import { updateHtmlExtraction } from '../../../lib/crawler/put_html_extraction';
import { deleteIndex } from '../../../lib/indices/delete_index';
import { RouteDependencies } from '../../../plugin';
import { createError } from '../../../utils/create_error';
@@ -389,6 +391,44 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
})
);
+ router.put(
+ {
+ path: '/internal/enterprise_search/indices/{indexName}/crawler/html_extraction',
+ validate: {
+ body: schema.object({
+ extract_full_html: schema.boolean(),
+ }),
+ params: schema.object({
+ indexName: schema.string(),
+ }),
+ },
+ },
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+
+ const connector = await fetchConnectorByIndexName(client, request.params.indexName);
+ if (
+ connector &&
+ connector.service_type === ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE
+ ) {
+ await updateHtmlExtraction(client, request.body.extract_full_html, connector);
+ return response.ok();
+ } else {
+ return createError({
+ errorCode: ErrorCode.RESOURCE_NOT_FOUND,
+ message: i18n.translate(
+ 'xpack.enterpriseSearch.server.routes.updateHtmlExtraction.noCrawlerFound',
+ {
+ defaultMessage: 'Could not find a crawler for this index',
+ }
+ ),
+ response,
+ statusCode: 404,
+ });
+ }
+ })
+ );
+
registerCrawlerCrawlRulesRoutes(routeDependencies);
registerCrawlerEntryPointRoutes(routeDependencies);
registerCrawlerSitemapRoutes(routeDependencies);
diff --git a/x-pack/plugins/fleet/server/services/agents/action_status.ts b/x-pack/plugins/fleet/server/services/agents/action_status.ts
index 24bb3e3b5b6b4..c50630784619a 100644
--- a/x-pack/plugins/fleet/server/services/agents/action_status.ts
+++ b/x-pack/plugins/fleet/server/services/agents/action_status.ts
@@ -23,7 +23,7 @@ export async function getActionStatuses(
options: ListWithKuery
): Promise {
const actions = await _getActions(esClient, options);
- const cancelledActions = await _getCancelledActions(esClient);
+ const cancelledActions = await getCancelledActions(esClient);
let acks: any;
try {
@@ -135,7 +135,7 @@ export async function getActionStatuses(
return results;
}
-async function _getCancelledActions(
+export async function getCancelledActions(
esClient: ElasticsearchClient
): Promise> {
const res = await esClient.search({
@@ -144,7 +144,7 @@ async function _getCancelledActions(
size: SO_SEARCH_LIMIT,
query: {
bool: {
- must: [
+ filter: [
{
term: {
type: 'CANCEL',
diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts
index 7c88b4885b843..95f78b4de989e 100644
--- a/x-pack/plugins/fleet/server/services/agents/actions.test.ts
+++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts
@@ -7,6 +7,9 @@
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
+import { createAppContextStartContractMock } from '../../mocks';
+import { appContextService } from '../app_context';
+
import { cancelAgentAction } from './actions';
import { bulkUpdateAgents } from './crud';
@@ -15,6 +18,13 @@ jest.mock('./crud');
const mockedBulkUpdateAgents = bulkUpdateAgents as jest.Mock;
describe('Agent actions', () => {
+ beforeEach(async () => {
+ appContextService.start(createAppContextStartContractMock());
+ });
+
+ afterEach(() => {
+ appContextService.stop();
+ });
describe('cancelAgentAction', () => {
it('throw if the target action is not found', async () => {
const esClient = elasticsearchServiceMock.createInternalClient();
@@ -28,7 +38,7 @@ describe('Agent actions', () => {
);
});
- it('should create one CANCEL action for each action found', async () => {
+ it('should create one CANCEL action for each UPGRADE action found', async () => {
const esClient = elasticsearchServiceMock.createInternalClient();
esClient.search.mockResolvedValue({
hits: {
@@ -38,6 +48,7 @@ describe('Agent actions', () => {
action_id: 'action1',
agents: ['agent1', 'agent2'],
expiration: '2022-05-12T18:16:18.019Z',
+ type: 'UPGRADE',
},
},
{
@@ -45,6 +56,7 @@ describe('Agent actions', () => {
action_id: 'action1',
agents: ['agent3', 'agent4'],
expiration: '2022-05-12T18:16:18.019Z',
+ type: 'UPGRADE',
},
},
],
diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts
index 80a23e4ecbb7b..44fde4d98b389 100644
--- a/x-pack/plugins/fleet/server/services/agents/actions.ts
+++ b/x-pack/plugins/fleet/server/services/agents/actions.ts
@@ -234,61 +234,110 @@ export async function getUnenrollAgentActions(
}
export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: string) {
- const res = await esClient.search({
- index: AGENT_ACTIONS_INDEX,
- query: {
- bool: {
- must: [
- {
- term: {
- action_id: actionId,
+ const getUpgradeActions = async () => {
+ const res = await esClient.search({
+ index: AGENT_ACTIONS_INDEX,
+ query: {
+ bool: {
+ filter: [
+ {
+ term: {
+ action_id: actionId,
+ },
},
- },
- ],
+ ],
+ },
},
- },
- size: SO_SEARCH_LIMIT,
- });
+ size: SO_SEARCH_LIMIT,
+ });
- if (res.hits.hits.length === 0) {
- throw new AgentActionNotFoundError('Action not found');
- }
+ if (res.hits.hits.length === 0) {
+ throw new AgentActionNotFoundError('Action not found');
+ }
+
+ const upgradeActions: FleetServerAgentAction[] = res.hits.hits
+ .map((hit) => hit._source as FleetServerAgentAction)
+ .filter(
+ (action: FleetServerAgentAction | undefined): boolean =>
+ !!action && !!action.agents && !!action.action_id && action.type === 'UPGRADE'
+ );
+ return upgradeActions;
+ };
const cancelActionId = uuidv4();
const now = new Date().toISOString();
- for (const hit of res.hits.hits) {
- if (!hit._source || !hit._source.agents || !hit._source.action_id) {
- continue;
- }
- if (hit._source.type === 'UPGRADE') {
- const errors = {};
- await bulkUpdateAgents(
- esClient,
- hit._source.agents.map((agentId) => ({
- agentId,
- data: {
- upgraded_at: null,
- upgrade_started_at: null,
- },
- })),
- errors
- );
- if (Object.keys(errors).length > 0) {
- appContextService
- .getLogger()
- .debug(`Errors while bulk updating agents for cancel action: ${JSON.stringify(errors)}`);
- }
- }
+
+ const cancelledActions: Array<{ agents: string[] }> = [];
+
+ const createAction = async (action: FleetServerAgentAction) => {
await createAgentAction(esClient, {
id: cancelActionId,
type: 'CANCEL',
- agents: hit._source.agents,
+ agents: action.agents!,
data: {
- target_id: hit._source.action_id,
+ target_id: action.action_id,
},
created_at: now,
- expiration: hit._source.expiration,
+ expiration: action.expiration,
});
+ cancelledActions.push({
+ agents: action.agents!,
+ });
+ };
+
+ let upgradeActions = await getUpgradeActions();
+ for (const action of upgradeActions) {
+ await createAction(action);
+ }
+
+ const updateAgentsToHealthy = async (action: FleetServerAgentAction) => {
+ appContextService
+ .getLogger()
+ .info(
+ `Moving back ${
+ action.agents!.length
+ } agents from updating to healthy state after cancel upgrade`
+ );
+ const errors = {};
+ await bulkUpdateAgents(
+ esClient,
+ action.agents!.map((agentId: string) => ({
+ agentId,
+ data: {
+ upgraded_at: null,
+ upgrade_started_at: null,
+ },
+ })),
+ errors
+ );
+ if (Object.keys(errors).length > 0) {
+ appContextService
+ .getLogger()
+ .info(`Errors while bulk updating agents for cancel action: ${JSON.stringify(errors)}`);
+ }
+ };
+
+ for (const action of upgradeActions) {
+ await updateAgentsToHealthy(action);
+ }
+
+ // At the end of cancel, doing one more query on upgrade action to find those docs that were possibly created by a concurrent upgrade action.
+ // This is to make sure we cancel all upgrade batches.
+ upgradeActions = await getUpgradeActions();
+ if (cancelledActions.length < upgradeActions.length) {
+ const missingBatches = upgradeActions.filter(
+ (upgradeAction) =>
+ !cancelledActions.some(
+ (cancelled) => upgradeAction.agents && cancelled.agents[0] === upgradeAction.agents[0]
+ )
+ );
+ appContextService.getLogger().debug(`missing batches to cancel: ${missingBatches.length}`);
+ if (missingBatches.length > 0) {
+ for (const missingBatch of missingBatches) {
+ await createAction(missingBatch);
+ await updateAgentsToHealthy(missingBatch);
+ }
+ }
}
return {
diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts
index 827044657324a..739619f7dc6b3 100644
--- a/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts
+++ b/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts
@@ -8,12 +8,22 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { appContextService } from '../app_context';
-
+import type { Agent } from '../../types';
import { createAppContextStartContractMock } from '../../mocks';
import { sendUpgradeAgentsActions } from './upgrade';
import { createClientMock } from './action.mock';
-import { getRollingUpgradeOptions } from './upgrade_action_runner';
+import { getRollingUpgradeOptions, upgradeBatch } from './upgrade_action_runner';
+
+jest.mock('./action_status', () => {
+ return {
+ getCancelledActions: jest.fn().mockResolvedValue([
+ {
+ actionId: 'cancelled-action',
+ },
+ ]),
+ };
+});
describe('sendUpgradeAgentsActions (plural)', () => {
beforeEach(async () => {
@@ -102,6 +112,14 @@ describe('sendUpgradeAgentsActions (plural)', () => {
expect(doc.upgraded_at).toEqual(null);
}
});
+
+ it('skip upgrade if action id is cancelled', async () => {
+ const { soClient, esClient, agentInRegularDoc } = createClientMock();
+ const agents = [{ id: agentInRegularDoc._id } as Agent];
+ await upgradeBatch(soClient, esClient, agents, {}, {
+ actionId: 'cancelled-action',
+ } as any);
+ });
});
describe('getRollingUpgradeOptions', () => {
diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts
index ff677db23cacc..8b7409375fb2a 100644
--- a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts
+++ b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts
@@ -25,6 +25,7 @@ import { bulkUpdateAgents } from './crud';
import { createErrorActionResults, createAgentAction } from './actions';
import { getHostedPolicies, isHostedAgent } from './hosted_agent';
import { BulkActionTaskType } from './bulk_action_types';
+import { getCancelledActions } from './action_status';
export class UpgradeActionRunner extends ActionRunner {
protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> {
@@ -40,6 +41,11 @@ export class UpgradeActionRunner extends ActionRunner {
}
}
+const isActionIdCancelled = async (esClient: ElasticsearchClient, actionId: string) => {
+ const cancelledActions = await getCancelledActions(esClient);
+ return cancelledActions.filter((action) => action.actionId === actionId).length > 0;
+};
+
export async function upgradeBatch(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
@@ -108,6 +114,24 @@ export async function upgradeBatch(
options.upgradeDurationSeconds
);
+ if (options.actionId && (await isActionIdCancelled(esClient, options.actionId))) {
+ appContextService
+ .getLogger()
+ .info(
+ `Skipping batch of actionId:${options.actionId} of ${givenAgents.length} agents as the upgrade was cancelled`
+ );
+ return {
+ actionId: options.actionId,
+ };
+ }
+ if (options.actionId) {
+ appContextService
+ .getLogger()
+ .info(
+ `Continuing batch of actionId:${options.actionId} of ${givenAgents.length} agents of upgrade`
+ );
+ }
+
await bulkUpdateAgents(
esClient,
agentsToUpdate.map((agent) => ({
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx
index 89bc192606944..0e4aa275a7879 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx
@@ -8,6 +8,7 @@
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { withTheme, EuiTheme } from '@kbn/kibana-react-plugin/common';
+import { KubernetesTour } from './kubernetes_tour';
interface Props {
'data-test-subj'?: string;
@@ -15,10 +16,26 @@ interface Props {
onClick: () => void;
theme: EuiTheme | undefined;
children: ReactNode;
+ showKubernetesInfo?: boolean;
}
+const ButtonLabel = ({ label, theme }: { label: string; theme?: EuiTheme }) => (
+
+ {label}
+
+);
+
export const DropdownButton = withTheme((props: Props) => {
- const { onClick, label, theme, children } = props;
+ const { onClick, label, theme, children, showKubernetesInfo } = props;
return (
{
boxShadow: `0px 3px 2px ${theme?.eui.euiTableActionsBorderColor}, 0px 1px 1px ${theme?.eui.euiTableActionsBorderColor}`,
}}
>
-
- {label}
-
+ {showKubernetesInfo ? (
+
+
+
+ ) : (
+
+ )}
{
+ const [isTourSeen, setIsTourSeen] = useLocalStorage(KUBERNETES_TOUR_STORAGE_KEY, false);
+ const markTourAsSeen = () => setIsTourSeen(true);
+
+ return (
+
+
+ {i18n.translate('xpack.infra.homePage.kubernetesTour.text', {
+ defaultMessage:
+ 'Click here to see your infrastructure in different ways, including Kubernetes pods.',
+ })}
+
+ }
+ isStepOpen={!isTourSeen}
+ maxWidth={350}
+ onFinish={markTourAsSeen}
+ step={1}
+ stepsTotal={1}
+ title={i18n.translate('xpack.infra.homePage.kubernetesTour.title', {
+ defaultMessage: 'Want a different view?',
+ })}
+ anchorPosition="downCenter"
+ footerAction={
+
+ {i18n.translate('xpack.infra.homePage.kubernetesTour.dismiss', {
+ defaultMessage: 'Dismiss',
+ })}
+
+ }
+ >
+ {children}
+
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx
new file mode 100644
index 0000000000000..b92e27c0b58f8
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiGlobalToastList } from '@elastic/eui';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
+import { useWaffleOptionsContext } from '../hooks/use_waffle_options';
+
+const KUBERNETES_TOAST_STORAGE_KEY = 'kubernetesToastKey';
+const KUBERNETES_FEEDBACK_LINK = 'https://ela.st/k8s-feedback';
+
+export const SurveyKubernetes = () => {
+ const { nodeType } = useWaffleOptionsContext();
+ const podNodeType: typeof nodeType = 'pod';
+
+ const [isToastSeen, setIsToastSeen] = useLocalStorage(KUBERNETES_TOAST_STORAGE_KEY, false);
+ const markToastAsSeen = () => setIsToastSeen(true);
+
+ return (
+ <>
+ {nodeType === podNodeType && (
+ <>
+
+
+
+ {!isToastSeen && (
+
+ ),
+ color: 'primary',
+ iconType: 'help',
+ toastLifeTimeMs: 0x7fffffff, // Biggest possible lifetime because we control when it should be visible using isToastSeen
+ text: (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ ),
+ },
+ ]}
+ />
+ )}
+ >
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx
index 6234dcc0bbf0b..e3ca9b770e2ef 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx
@@ -124,6 +124,7 @@ export const WaffleInventorySwitcher: React.FC = () => {
data-test-subj={'openInventorySwitcher'}
onClick={openPopover}
label={i18n.translate('xpack.infra.waffle.showLabel', { defaultMessage: 'Show' })}
+ showKubernetesInfo={true}
>
{selectedText}
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx
index 77b8e9a35a64e..fde2a4923441d 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx
@@ -23,6 +23,7 @@ import { inventoryTitle } from '../../../translations';
import { SavedViews } from './components/saved_views';
import { SnapshotContainer } from './components/snapshot_container';
import { fullHeightContentStyles } from '../../../page_template.styles';
+import { SurveyKubernetes } from './components/survey_kubernetes';
export const SnapshotPage = () => {
const {
@@ -59,7 +60,7 @@ export const SnapshotPage = () => {
hasData={metricIndicesExist}
pageHeader={{
pageTitle: inventoryTitle,
- rightSideItems: [ ],
+ rightSideItems: [ , ],
}}
pageSectionProps={{
contentProps: {
diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts
index abd8a48815122..bb27eec2076e3 100644
--- a/x-pack/plugins/lens/public/data_views_service/loader.ts
+++ b/x-pack/plugins/lens/public/data_views_service/loader.ts
@@ -176,7 +176,9 @@ export async function loadIndexPatterns({
}
indexPatterns.push(
...(await Promise.all(
- Object.values(adHocDataViews || {}).map((spec) => dataViews.create(spec))
+ Object.values(adHocDataViews || {}).map((spec) =>
+ dataViews.create({ ...spec, allowNoIndex: true })
+ )
))
);
diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx
index be43974b7ba44..3471c82a43fa4 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx
@@ -980,9 +980,6 @@ describe('last_value', () => {
"dimensionId": "col1",
"id": "dimensionButton",
},
- Object {
- "id": "visualization",
- },
Object {
"id": "embeddableBadge",
},
diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx
index 46a9335f20d1c..1f0e50ecb2ac6 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx
@@ -86,7 +86,6 @@ function getInvalidSortFieldMessage(
displayLocations: [
{ id: 'toolbar' },
{ id: 'dimensionButton', dimensionId: columnId },
- { id: 'visualization' },
{ id: 'embeddableBadge' },
],
};
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx
index b272b5ec3e362..d4319f13e9b0f 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx
@@ -156,6 +156,40 @@ describe('BuilderEntryItem', () => {
expect(wrapper.find('.euiFormHelpText.euiFormRow__text').exists()).toBeFalsy();
});
+ test('it render mapping issues warning text when field has mapping conflicts', () => {
+ wrapper = mount(
+
+ );
+
+ expect(wrapper.find('.euiFormHelpText.euiFormRow__text').text()).toMatch(
+ /This field is defined as several types across different indices./
+ );
+ });
+
test('it renders field values correctly when operator is "isOperator"', () => {
wrapper = mount(
= ({
operatorsList,
allowCustomOptions = false,
}): JSX.Element => {
+ const sPaddingSize = useEuiPaddingSize('s');
+
const handleError = useCallback(
(err: boolean): void => {
setErrorsExist(err);
@@ -194,49 +206,82 @@ export const BuilderEntryItem: React.FC = ({
onChange={handleFieldChange}
acceptsCustomOptions={entry.nested == null}
data-test-subj="exceptionBuilderEntryField"
+ showMappingConflicts={true}
/>
);
- if (isFirst) {
+ const warningIconCss = { marginRight: `${sPaddingSize}` };
+ const getMappingConflictsWarning = (field: DataViewFieldBase): React.ReactNode | null => {
+ const conflictsInfo = getMappingConflictsInfo(field);
+ if (!conflictsInfo) {
+ return null;
+ }
return (
-
- {comboBox}
-
+ <>
+
+
+
+ {i18n.FIELD_CONFLICT_INDICES_WARNING_DESCRIPTION}
+ >
+ }
+ arrowDisplay="none"
+ >
+ {conflictsInfo.map((info) => {
+ const groupDetails = info.groupedIndices.map(
+ ({ name, count }) =>
+ `${count > 1 ? i18n.CONFLICT_MULTIPLE_INDEX_DESCRIPTION(name, count) : name}`
+ );
+ return (
+ <>
+
+ {`${
+ info.totalIndexCount > 1
+ ? i18n.CONFLICT_MULTIPLE_INDEX_DESCRIPTION(info.type, info.totalIndexCount)
+ : info.type
+ }: ${groupDetails.join(', ')}`}
+ >
+ );
+ })}
+
+
+ >
);
- } else {
- return (
-
- {comboBox}
-
+ };
+
+ const customOptionText =
+ entry.nested == null && allowCustomOptions ? i18n.CUSTOM_COMBOBOX_OPTION_TEXT : undefined;
+ const helpText =
+ entry.field?.type !== 'conflict' ? (
+ customOptionText
+ ) : (
+ <>
+ {customOptionText}
+ {getMappingConflictsWarning(entry.field)}
+ >
);
- }
+ return (
+
+ {comboBox}
+
+ );
},
[
indexPattern,
entry,
listType,
listTypeSpecificIndexPatternFilter,
- handleFieldChange,
osTypes,
isDisabled,
+ handleFieldChange,
+ sPaddingSize,
allowCustomOptions,
]
);
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/translations.ts b/x-pack/plugins/lists/public/exceptions/components/builder/translations.ts
index ee7971e69c83a..c394a9daf674c 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/translations.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/translations.ts
@@ -83,3 +83,16 @@ export const CUSTOM_COMBOBOX_OPTION_TEXT = i18n.translate(
'Select a field from the list. If your field is not available, create a custom one.',
}
);
+
+export const FIELD_CONFLICT_INDICES_WARNING_DESCRIPTION = i18n.translate(
+ 'xpack.lists.exceptions.field.mappingConflict.description',
+ {
+ defaultMessage: 'This field is defined as several types across different indices.',
+ }
+);
+
+export const CONFLICT_MULTIPLE_INDEX_DESCRIPTION = (name: string, count: number): string =>
+ i18n.translate('xpack.lists.exceptions.field.index.description', {
+ defaultMessage: '{name} ({count} indices)',
+ values: { count, name },
+ });
diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json
index b794f3ba39a03..bf734f14afe1d 100644
--- a/x-pack/plugins/ml/kibana.json
+++ b/x-pack/plugins/ml/kibana.json
@@ -50,6 +50,7 @@
"lens",
"maps",
"savedObjects",
+ "savedSearch",
"usageCollection",
"unifiedFieldList",
"unifiedSearch"
diff --git a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx
index 3ee53940add2b..b3d3a537b9be1 100644
--- a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx
+++ b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx
@@ -25,7 +25,7 @@ export const ChangePointDetectionPage: FC = () => {
const context = useMlContext();
const dataView = context.currentDataView;
- const savedSearch = context.currentSavedSearch;
+ const savedSearch = context.selectedSavedSearch;
return (
<>
diff --git a/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx b/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx
index c7abf7385c3b0..031297c55929a 100644
--- a/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx
+++ b/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx
@@ -25,7 +25,7 @@ export const ExplainLogRateSpikesPage: FC = () => {
const context = useMlContext();
const dataView = context.currentDataView;
- const savedSearch = context.currentSavedSearch;
+ const savedSearch = context.selectedSavedSearch;
return (
<>
diff --git a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx
index a78a1e3815be8..cdc0c079d541c 100644
--- a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx
+++ b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx
@@ -25,7 +25,7 @@ export const LogCategorizationPage: FC = () => {
const context = useMlContext();
const dataView = context.currentDataView;
- const savedSearch = context.currentSavedSearch;
+ const savedSearch = context.selectedSavedSearch;
return (
<>
diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx
index d10eb2c8e63ec..5e5099937c7e1 100644
--- a/x-pack/plugins/ml/public/application/app.tsx
+++ b/x-pack/plugins/ml/public/application/app.tsx
@@ -78,6 +78,10 @@ const App: FC = ({ coreStart, deps, appMountParams }) => {
config: coreStart.uiSettings!,
setBreadcrumbs: coreStart.chrome!.setBreadcrumbs,
redirectToMlAccessDeniedPage,
+ getSavedSearchDeps: {
+ search: deps.data.search,
+ savedObjectsClient: coreStart.savedObjects.client,
+ },
};
const services = {
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
index 4e5a172e70aa8..30e711d9f41b9 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
@@ -15,6 +15,7 @@ import {
EuiDataGridColumn,
} from '@elastic/eui';
+import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
import { Dictionary } from '../../../../common/types/common';
import { ChartData } from '../../../../common/types/field_histograms';
@@ -88,6 +89,7 @@ export interface UseIndexDataReturnType
> {
renderCellValue: RenderCellValue;
indexPatternFields?: string[];
+ timeRangeMs?: TimeRangeMs;
}
export interface UseDataGridReturnType {
diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts b/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts
index a2ce2dcea2273..8cd013732b726 100644
--- a/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts
+++ b/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts
@@ -7,11 +7,11 @@
import { FC } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
-import { SavedSearchSavedObject } from '../../../../common/types/kibana';
+import { type SavedSearch } from '@kbn/saved-search-plugin/public';
declare const DataRecognizer: FC<{
indexPattern: DataView;
- savedSearch: SavedSearchSavedObject | null;
+ savedSearch: SavedSearch | null;
results: {
count: number;
onChange?: Function;
diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/eui_combo_box_with_field_stats.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/eui_combo_box_with_field_stats.tsx
new file mode 100644
index 0000000000000..bd02a4612ce52
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/eui_combo_box_with_field_stats.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC, useMemo } from 'react';
+import { EuiComboBoxProps } from '@elastic/eui/src/components/combo_box/combo_box';
+import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { useFieldStatsTrigger } from './use_field_stats_trigger';
+
+export const optionCss = css`
+ .euiComboBoxOption__enterBadge {
+ display: none;
+ }
+ .euiFlexGroup {
+ gap: 0px;
+ }
+ .euiComboBoxOption__content {
+ margin-left: 2px;
+ }
+`;
+
+export const EuiComboBoxWithFieldStats: FC<
+ EuiComboBoxProps
+> = ({ options, ...restProps }) => {
+ const { renderOption } = useFieldStatsTrigger();
+ const comboBoxOptions: EuiComboBoxOptionOption[] = useMemo(
+ () =>
+ Array.isArray(options)
+ ? options.map((o) => ({
+ ...o,
+ css: optionCss,
+ }))
+ : [],
+ [options]
+ );
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_content.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_content.tsx
index cf04337e06d04..ddf082cbd7e96 100644
--- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_content.tsx
+++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_content.tsx
@@ -5,69 +5,65 @@
* 2.0.
*/
-import React, { FC, useContext, useEffect, useMemo, useState } from 'react';
-import { FieldStats, FieldStatsServices } from '@kbn/unified-field-list-plugin/public';
-import moment from 'moment';
+import React, { FC, useMemo } from 'react';
+import {
+ FieldStats,
+ FieldStatsProps,
+ FieldStatsServices,
+} from '@kbn/unified-field-list-plugin/public';
import { isDefined } from '@kbn/ml-is-defined';
+import type { DataView } from '@kbn/data-plugin/common';
+import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
+import moment from 'moment';
+import { euiPaletteColorBlind } from '@elastic/eui';
import { getDefaultDatafeedQuery } from '../../jobs/new_job/utils/new_job_utils';
-import { useMlKibana } from '../../contexts/kibana';
-import { JobCreatorContext } from '../../jobs/new_job/pages/components/job_creator_context';
import { useFieldStatsFlyoutContext } from './use_field_stats_flytout_context';
-const defaultDatafeedQuery = getDefaultDatafeedQuery();
+const DEFAULT_DSL_QUERY = getDefaultDatafeedQuery();
+const DEFAULT_COLOR = euiPaletteColorBlind()[0];
-export const FieldStatsContent: FC = () => {
- const {
- services: { uiSettings, data, fieldFormats, charts },
- } = useMlKibana();
- const fieldStatsServices: FieldStatsServices = {
- uiSettings,
- dataViews: data.dataViews,
- data,
- fieldFormats,
- charts,
- };
- const { jobCreator, jobCreatorUpdated } = useContext(JobCreatorContext);
+export const FieldStatsContent: FC<{
+ dataView: DataView;
+ fieldStatsServices: FieldStatsServices;
+ timeRangeMs?: TimeRangeMs;
+ dslQuery?: FieldStatsProps['dslQuery'];
+}> = ({ dataView: currentDataView, fieldStatsServices, timeRangeMs, dslQuery }) => {
const { fieldName } = useFieldStatsFlyoutContext();
- const [start, setStart] = useState(jobCreator.start);
- const [end, setEnd] = useState(jobCreator.end);
-
- useEffect(() => {
- if (jobCreator.start !== start || jobCreator.end !== end) {
- setStart(jobCreator.start);
- setEnd(jobCreator.end);
+ // Format timestamp to ISO formatted date strings
+ const timeRange = useMemo(() => {
+ // Use the provided timeRange if available
+ if (timeRangeMs) {
+ return {
+ from: moment(timeRangeMs.from).toISOString(),
+ to: moment(timeRangeMs.to).toISOString(),
+ };
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [jobCreatorUpdated]);
-
- // Format timestamp to ISO formatted date strings
- const timeRange = useMemo(
- () =>
- start && end
- ? { from: moment(start).toISOString(), to: moment(end).toISOString() }
- : undefined,
- [start, end]
- );
+ // If time range is available via jobCreator, use that
+ // else mimic Discover and set timeRange to be now for data view without time field
+ const now = moment();
+ return { from: now.toISOString(), to: now.toISOString() };
+ }, [timeRangeMs]);
const fieldForStats = useMemo(
- () => (isDefined(fieldName) ? jobCreator.dataView.getFieldByName(fieldName) : undefined),
- [fieldName, jobCreator.dataView]
+ () => (isDefined(fieldName) ? currentDataView.getFieldByName(fieldName) : undefined),
+ [fieldName, currentDataView]
);
- const showFieldStats = timeRange && isDefined(jobCreator.dataViewId) && fieldForStats;
+ const showFieldStats = timeRange && isDefined(currentDataView) && fieldForStats;
return showFieldStats ? (
) : null;
};
diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout.tsx
index 31d949a0defca..89d53a1fe0ba3 100644
--- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout.tsx
@@ -19,10 +19,18 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { css } from '@emotion/react';
+import { type DataView } from '@kbn/data-plugin/common';
+import type { FieldStatsProps, FieldStatsServices } from '@kbn/unified-field-list-plugin/public';
+import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
import { useFieldStatsFlyoutContext } from './use_field_stats_flytout_context';
import { FieldStatsContent } from './field_stats_content';
-export const FieldStatsFlyout: FC = () => {
+export const FieldStatsFlyout: FC<{
+ dataView: DataView;
+ fieldStatsServices: FieldStatsServices;
+ timeRangeMs?: TimeRangeMs;
+ dslQuery?: FieldStatsProps['dslQuery'];
+}> = ({ dataView, fieldStatsServices, timeRangeMs, dslQuery }) => {
const { setIsFlyoutVisible, isFlyoutVisible, fieldName } = useFieldStatsFlyoutContext();
const closeFlyout = useCallback(() => setIsFlyoutVisible(false), []); // eslint-disable-line react-hooks/exhaustive-deps
@@ -50,7 +58,12 @@ export const FieldStatsFlyout: FC = () => {
>
{fieldName}
-
+
diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx
index 49d7189451313..f65c57cc52f2f 100644
--- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx
+++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx
@@ -6,10 +6,26 @@
*/
import React, { useCallback, useState } from 'react';
-import { FieldStatsFlyout } from './field_stats_flyout';
+import type { DataView } from '@kbn/data-plugin/common';
+import type { FieldStatsServices } from '@kbn/unified-field-list-plugin/public';
+import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
+import type { FieldStatsProps } from '@kbn/unified-field-list-plugin/public';
import { MLJobWizardFieldStatsFlyoutContext } from './use_field_stats_flytout_context';
+import { FieldStatsFlyout } from './field_stats_flyout';
-export const FieldStatsFlyoutProvider = ({ children }: { children: React.ReactElement }) => {
+export const FieldStatsFlyoutProvider = ({
+ dataView,
+ fieldStatsServices,
+ timeRangeMs,
+ dslQuery,
+ children,
+}: {
+ dataView: DataView;
+ fieldStatsServices: FieldStatsServices;
+ timeRangeMs?: TimeRangeMs;
+ dslQuery?: FieldStatsProps['dslQuery'];
+ children: React.ReactElement;
+}) => {
const [isFieldStatsFlyoutVisible, setFieldStatsIsFlyoutVisible] = useState(false);
const [fieldName, setFieldName] = useState();
const [fieldValue, setFieldValue] = useState();
@@ -31,7 +47,12 @@ export const FieldStatsFlyoutProvider = ({ children }: { children: React.ReactEl
fieldValue,
}}
>
-
+
{children}
);
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/components/field_stats_info_button.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_info_button.tsx
similarity index 87%
rename from x-pack/plugins/ml/public/application/jobs/new_job/common/components/field_stats_info_button.tsx
rename to x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_info_button.tsx
index 48386731c24c0..dd500416c8fb6 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/components/field_stats_info_button.tsx
+++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_info_button.tsx
@@ -9,19 +9,20 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiHighlight, EuiToolTip } fr
import { i18n } from '@kbn/i18n';
import React from 'react';
import { FieldIcon } from '@kbn/react-field';
-import { EVENT_RATE_FIELD_ID, Field } from '../../../../../../common/types/fields';
-import { getKbnFieldIconType } from '../../../../../../common/util/get_field_icon_types';
+import { EVENT_RATE_FIELD_ID, Field } from '../../../../common/types/fields';
+import { getKbnFieldIconType } from '../../../../common/util/get_field_icon_types';
+export type FieldForStats = Pick;
export const FieldStatsInfoButton = ({
field,
label,
searchValue = '',
onButtonClick,
}: {
- field: Field;
+ field: FieldForStats;
label: string;
searchValue?: string;
- onButtonClick?: (field: Field) => void;
+ onButtonClick?: (field: FieldForStats) => void;
}) => {
return (
diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/index.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/index.ts
index bc20a20a36c06..c808653bb9ff7 100644
--- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/index.ts
+++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/index.ts
@@ -12,3 +12,6 @@ export {
MLJobWizardFieldStatsFlyoutContext,
useFieldStatsFlyoutContext,
} from './use_field_stats_flytout_context';
+export { FieldStatsInfoButton } from './field_stats_info_button';
+export { useFieldStatsTrigger } from './use_field_stats_trigger';
+export { EuiComboBoxWithFieldStats } from './eui_combo_box_with_field_stats';
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/utils/use_field_stats_trigger.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx
similarity index 68%
rename from x-pack/plugins/ml/public/application/jobs/new_job/utils/use_field_stats_trigger.tsx
rename to x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx
index f13de61bef48d..0106385b3eae3 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/utils/use_field_stats_trigger.tsx
+++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx
@@ -7,30 +7,21 @@
import React, { ReactNode, useCallback } from 'react';
import { EuiComboBoxOptionOption } from '@elastic/eui';
-import { css } from '@emotion/react';
-import { useFieldStatsFlyoutContext } from '../../../components/field_stats_flyout';
-import { FieldStatsInfoButton } from '../common/components/field_stats_info_button';
-import { Field } from '../../../../../common/types/fields';
+import { optionCss } from './eui_combo_box_with_field_stats';
+import { useFieldStatsFlyoutContext } from '.';
+import { FieldForStats, FieldStatsInfoButton } from './field_stats_info_button';
+import { Field } from '../../../../common/types/fields';
interface Option extends EuiComboBoxOptionOption {
field: Field;
}
-const optionCss = css`
- .euiComboBoxOption__enterBadge {
- display: none;
- }
- .euiFlexGroup {
- gap: 0px;
- }
- .euiComboBoxOption__content {
- margin-left: 2px;
- }
-`;
-
export const useFieldStatsTrigger = () => {
const { setIsFlyoutVisible, setFieldName } = useFieldStatsFlyoutContext();
+
+ const closeFlyout = useCallback(() => setIsFlyoutVisible(false), [setIsFlyoutVisible]);
+
const handleFieldStatsButtonClick = useCallback(
- (field: Field) => {
+ (field: FieldForStats) => {
if (typeof field.id === 'string') {
setFieldName(field.id);
setIsFlyoutVisible(true);
@@ -38,7 +29,6 @@ export const useFieldStatsTrigger = () => {
},
[setFieldName, setIsFlyoutVisible]
);
-
const renderOption = useCallback(
(option: EuiComboBoxOptionOption, searchValue: string): ReactNode => {
const field = (option as Option).field;
@@ -54,5 +44,12 @@ export const useFieldStatsTrigger = () => {
},
[handleFieldStatsButtonClick]
);
- return { renderOption, setIsFlyoutVisible, setFieldName, handleFieldStatsButtonClick, optionCss };
+ return {
+ renderOption,
+ setIsFlyoutVisible,
+ setFieldName,
+ handleFieldStatsButtonClick,
+ closeFlyout,
+ optionCss,
+ };
};
diff --git a/x-pack/plugins/ml/public/application/contexts/ml/__mocks__/kibana_context_value.ts b/x-pack/plugins/ml/public/application/contexts/ml/__mocks__/kibana_context_value.ts
index 642bc4baee712..1e783660a338e 100644
--- a/x-pack/plugins/ml/public/application/contexts/ml/__mocks__/kibana_context_value.ts
+++ b/x-pack/plugins/ml/public/application/contexts/ml/__mocks__/kibana_context_value.ts
@@ -8,7 +8,10 @@
import { dataViewMock } from './data_view';
import { dataViewsContractMock } from './data_view_contract';
import { kibanaConfigMock } from './kibana_config';
-import { savedSearchMock } from './saved_search';
+import { deprecatedSavedSearchSimpleObjMock } from './saved_search';
+import { SavedSearch } from '@kbn/saved-search-plugin/public';
+
+const mockSavedSearch: SavedSearch = {} as unknown as SavedSearch;
export const kibanaContextValueMock = {
combinedQuery: {
@@ -16,7 +19,8 @@ export const kibanaContextValueMock = {
language: 'the-query-language',
},
currentDataView: dataViewMock,
- currentSavedSearch: savedSearchMock,
+ deprecatedSavedSearchObj: deprecatedSavedSearchSimpleObjMock,
+ selectedSavedSearch: mockSavedSearch,
dataViewsContract: dataViewsContractMock,
kibanaConfig: kibanaConfigMock,
};
diff --git a/x-pack/plugins/ml/public/application/contexts/ml/__mocks__/saved_search.ts b/x-pack/plugins/ml/public/application/contexts/ml/__mocks__/saved_search.ts
index b9475996c79d8..f16b4f311f2e4 100644
--- a/x-pack/plugins/ml/public/application/contexts/ml/__mocks__/saved_search.ts
+++ b/x-pack/plugins/ml/public/application/contexts/ml/__mocks__/saved_search.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-export const savedSearchMock: any = {
+export const deprecatedSavedSearchSimpleObjMock: any = {
id: 'the-saved-search-id',
type: 'search',
attributes: {
diff --git a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
index 8b755b02f99b9..65a14111a8a8f 100644
--- a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
+++ b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
@@ -7,13 +7,16 @@
import React from 'react';
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
+import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import type { SavedSearchSavedObject } from '../../../../common/types/kibana';
import type { MlServicesContext } from '../../app';
export interface MlContextValue {
combinedQuery: any;
currentDataView: DataView; // TODO this should be DataView or null
- currentSavedSearch: SavedSearchSavedObject | null;
+ // @deprecated currentSavedSearch is of SavedSearchSavedObject type, change to selectedSavedSearch
+ deprecatedSavedSearchObj: SavedSearchSavedObject | null;
+ selectedSavedSearch: SavedSearch | null;
dataViewsContract: DataViewsContract;
kibanaConfig: any; // IUiSettingsClient;
kibanaVersion: string;
diff --git a/x-pack/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts b/x-pack/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts
deleted file mode 100644
index 1d55d0d454725..0000000000000
--- a/x-pack/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts
+++ /dev/null
@@ -1,20 +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 { useContext } from 'react';
-
-import { MlContext } from './ml_context';
-
-export const useCurrentSavedSearch = () => {
- const context = useContext(MlContext);
-
- if (context.currentSavedSearch === undefined) {
- throw new Error('currentSavedSearch is undefined');
- }
-
- return context.currentSavedSearch;
-};
diff --git a/x-pack/plugins/ml/public/application/contexts/ml/use_ml_context.ts b/x-pack/plugins/ml/public/application/contexts/ml/use_ml_context.ts
index 1a07cb0338855..f9ee39921087f 100644
--- a/x-pack/plugins/ml/public/application/contexts/ml/use_ml_context.ts
+++ b/x-pack/plugins/ml/public/application/contexts/ml/use_ml_context.ts
@@ -15,7 +15,10 @@ export const useMlContext = () => {
if (
context.combinedQuery === undefined ||
context.currentDataView === undefined ||
- context.currentSavedSearch === undefined ||
+ // @deprecated currentSavedSearch is of SavedSearchSavedObject type
+ // and should be migrated to selectedSavedSearch
+ context.deprecatedSavedSearchObj === undefined ||
+ context.selectedSavedSearch === undefined ||
context.dataViewsContract === undefined ||
context.kibanaConfig === undefined
) {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
index 85157569e64fb..9608457723c08 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
@@ -12,6 +12,12 @@ import { isEqual } from 'lodash';
import { LEFT_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
+import { ES_FIELD_TYPES } from '@kbn/field-types';
+import { useFieldStatsTrigger } from '../../../../../components/field_stats_flyout/use_field_stats_trigger';
+import {
+ FieldForStats,
+ FieldStatsInfoButton,
+} from '../../../../../components/field_stats_flyout/field_stats_info_button';
import { FieldSelectionItem } from '../../../../../../../common/types/data_frame_analytics';
// @ts-ignore could not find declaration file
import { CustomSelectionTable } from '../../../../../components/custom_selection_table';
@@ -23,62 +29,6 @@ const minimumFieldsMessage = i18n.translate(
}
);
-const columns = [
- {
- id: 'checkbox',
- isCheckbox: true,
- textOnly: false,
- width: '32px',
- },
- {
- label: i18n.translate(
- 'xpack.ml.dataframe.analytics.create.analysisFieldsTable.fieldNameColumn',
- {
- defaultMessage: 'Field name',
- }
- ),
- id: 'name',
- isSortable: true,
- alignment: LEFT_ALIGNMENT,
- },
- {
- id: 'mapping_types',
- label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.mappingColumn', {
- defaultMessage: 'Mapping',
- }),
- isSortable: false,
- alignment: LEFT_ALIGNMENT,
- },
- {
- label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.isIncludedColumn', {
- defaultMessage: 'Is included',
- }),
- id: 'is_included',
- alignment: LEFT_ALIGNMENT,
- isSortable: true,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- render: ({ is_included }: { is_included: boolean }) => (is_included ? 'Yes' : 'No'),
- },
- {
- label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.isRequiredColumn', {
- defaultMessage: 'Is required',
- }),
- id: 'is_required',
- alignment: LEFT_ALIGNMENT,
- isSortable: true,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- render: ({ is_required }: { is_required: boolean }) => (is_required ? 'Yes' : 'No'),
- },
- {
- label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.reasonColumn', {
- defaultMessage: 'Reason',
- }),
- id: 'reason',
- alignment: LEFT_ALIGNMENT,
- isSortable: false,
- },
-];
-
const checkboxDisabledCheck = (item: FieldSelectionItem) =>
item.is_required === true || (item.reason && item.reason.includes('unsupported type'));
@@ -110,6 +60,89 @@ export const AnalysisFieldsTable: FC<{
itemsPerPage: number;
}>({ pageIndex: 0, itemsPerPage: 5 });
+ const { handleFieldStatsButtonClick } = useFieldStatsTrigger();
+
+ const columns = [
+ {
+ id: 'checkbox',
+ isCheckbox: true,
+ textOnly: false,
+ width: '32px',
+ },
+ {
+ label: i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.analysisFieldsTable.fieldNameColumn',
+ {
+ defaultMessage: 'Field name',
+ }
+ ),
+ id: 'name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ render: ({ name, mapping_types }: { name: string; mapping_types: string[] }) => {
+ const field: FieldForStats = {
+ id: name,
+ type: (Array.isArray(mapping_types) && mapping_types.length > 0
+ ? mapping_types[0]
+ : 'number') as ES_FIELD_TYPES,
+ };
+ return (
+ <>
+
+ >
+ );
+ },
+
+ isSortable: true,
+ alignment: LEFT_ALIGNMENT,
+ },
+ {
+ id: 'mapping_types',
+ label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.mappingColumn', {
+ defaultMessage: 'Mapping',
+ }),
+ isSortable: false,
+ alignment: LEFT_ALIGNMENT,
+ },
+ {
+ label: i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.analyticsTable.isIncludedColumn',
+ {
+ defaultMessage: 'Is included',
+ }
+ ),
+ id: 'is_included',
+ alignment: LEFT_ALIGNMENT,
+ isSortable: true,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ render: ({ is_included }: { is_included: boolean }) => (is_included ? 'Yes' : 'No'),
+ },
+ {
+ label: i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.analyticsTable.isRequiredColumn',
+ {
+ defaultMessage: 'Is required',
+ }
+ ),
+ id: 'is_required',
+ alignment: LEFT_ALIGNMENT,
+ isSortable: true,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ render: ({ is_required }: { is_required: boolean }) => (is_required ? 'Yes' : 'No'),
+ },
+ {
+ label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.reasonColumn', {
+ defaultMessage: 'Reason',
+ }),
+ id: 'reason',
+ alignment: LEFT_ALIGNMENT,
+ isSortable: false,
+ },
+ ];
+
useEffect(() => {
if (includes.length === 0 && tableItems.length > 0) {
const includedFields: string[] = [];
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
index 052c4e64272c9..9da07dc0e6116 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
@@ -8,7 +8,6 @@
import React, { FC, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
EuiBadge,
- EuiComboBox,
EuiComboBoxOptionOption,
EuiFormRow,
EuiPanel,
@@ -20,6 +19,14 @@ import { i18n } from '@kbn/i18n';
import { debounce, cloneDeep } from 'lodash';
import { Query } from '@kbn/data-plugin/common/query';
+import { ES_FIELD_TYPES } from '@kbn/field-types';
+import { FieldStatsServices } from '@kbn/unified-field-list-plugin/public';
+import { useMlKibana } from '../../../../../contexts/kibana';
+import {
+ EuiComboBoxWithFieldStats,
+ FieldStatsFlyoutProvider,
+} from '../../../../../components/field_stats_flyout';
+import { FieldForStats } from '../../../../../components/field_stats_flyout/field_stats_info_button';
import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useMlContext } from '../../../../../contexts/ml';
import { getCombinedRuntimeMappings } from '../../../../../components/data_grid/common';
@@ -81,13 +88,26 @@ function getIndexDataQuery(savedSearchQuery: SavedSearchQuery, jobConfigQuery: a
return savedSearchQuery !== null ? savedSearchQuery : jobConfigQuery;
}
+type RuntimeMappingFieldType =
+ | ES_FIELD_TYPES.BOOLEAN
+ | ES_FIELD_TYPES.DATE
+ | ES_FIELD_TYPES.DOUBLE
+ | ES_FIELD_TYPES.GEO_POINT
+ | ES_FIELD_TYPES.IP
+ | ES_FIELD_TYPES.KEYWORD
+ | ES_FIELD_TYPES.LONG;
+
+interface RuntimeOption extends EuiComboBoxOptionOption {
+ field: FieldForStats;
+}
function getRuntimeDepVarOptions(jobType: AnalyticsJobType, runtimeMappings: RuntimeMappingsType) {
- const runtimeOptions: EuiComboBoxOptionOption[] = [];
+ const runtimeOptions: RuntimeOption[] = [];
Object.keys(runtimeMappings).forEach((id) => {
const field = runtimeMappings[id];
if (isRuntimeField(field) && shouldAddAsDepVarOption(id, field.type, jobType)) {
runtimeOptions.push({
label: id,
+ field: { id, type: field.type as RuntimeMappingFieldType },
});
}
});
@@ -101,7 +121,7 @@ export const ConfigurationStepForm: FC = ({
setCurrentStep,
}) => {
const mlContext = useMlContext();
- const { currentSavedSearch, currentDataView } = mlContext;
+ const { currentDataView, selectedSavedSearch } = mlContext;
const { savedSearchQuery, savedSearchQueryStr } = useSavedSearch();
const [fieldOptionsFetchFail, setFieldOptionsFetchFail] = useState(false);
@@ -207,6 +227,7 @@ export const ConfigurationStepForm: FC = ({
if (shouldAddAsDepVarOption(field.id, field.type, jobType)) {
depVarOptions.push({
label: field.id,
+ field,
});
if (formState.dependentVariable === field.id) {
@@ -532,6 +553,17 @@ export const ConfigurationStepForm: FC = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
[dependentVariableEmpty, jobType, scatterplotMatrixProps.fields.length]
);
+ const { services } = useMlKibana();
+ const fieldStatsServices: FieldStatsServices = useMemo(() => {
+ const { uiSettings, data, fieldFormats, charts } = services;
+ return {
+ uiSettings,
+ dataViews: data.dataViews,
+ data,
+ fieldFormats,
+ charts,
+ };
+ }, [services]);
// Don't render until `savedSearchQuery` has been initialized.
// `undefined` means uninitialized, `null` means initialized but not used.
@@ -543,219 +575,229 @@ export const ConfigurationStepForm: FC = ({
: indexPatternFieldsTableItems;
return (
-
-
-
-
- {savedSearchQuery === null && (
+
+
+
+
+
+ {savedSearchQuery === null && (
+
+
+
+ )}
+ {((isClone && cloneJob) || !isClone) && }
+ {savedSearchQuery !== null && (
+
+ {i18n.translate('xpack.ml.dataframe.analytics.create.savedSearchLabel', {
+ defaultMessage: 'Saved search',
+ })}
+
+ )}
+
+ {selectedSavedSearch !== null
+ ? selectedSavedSearch.title
+ : currentDataView.getName()}
+
+
+ }
fullWidth
>
-
+
- )}
- {((isClone && cloneJob) || !isClone) && }
-
- {savedSearchQuery !== null && (
-
- {i18n.translate('xpack.ml.dataframe.analytics.create.savedSearchLabel', {
- defaultMessage: 'Saved search',
- })}
-
- )}
-
- {savedSearchQuery !== null
- ? currentSavedSearch?.attributes.title
- : currentDataView.title}
-
-
- }
- fullWidth
- >
-
-
- {isJobTypeWithDepVar && (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.create.dependentVariableOptionsFetchError',
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.dependentVariableOptionsFetchError',
+ {
+ defaultMessage:
+ 'There was a problem fetching fields. Please refresh the page and try again.',
+ }
+ )}
+ ,
+ ]
+ : []),
+ ...(fieldOptionsFetchFail === true && maxDistinctValuesError !== undefined
+ ? [
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.dependentVariableMaxDistictValuesError',
+ {
+ defaultMessage: 'Invalid. {message}',
+ values: { message: maxDistinctValuesError },
+ }
+ )}
+ ,
+ ]
+ : []),
+ ]}
+ >
+ ,
- ]
- : []),
- ...(fieldOptionsFetchFail === true && maxDistinctValuesError !== undefined
- ? [
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.create.dependentVariableMaxDistictValuesError',
+ )
+ : i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.dependentVariableClassificationPlaceholder',
{
- defaultMessage: 'Invalid. {message}',
- values: { message: maxDistinctValuesError },
+ defaultMessage:
+ 'Select the numeric, categorical, or boolean field that you want to predict.',
}
- )}
- ,
- ]
- : []),
- ]}
- >
- {
+ setFormState({
+ dependentVariable: selectedOptions[0].label || '',
+ });
+ }}
+ isClearable={false}
+ isInvalid={dependentVariable === ''}
+ data-test-subj={`mlAnalyticsCreateJobWizardDependentVariableSelect${
+ loadingDepVarOptions ? ' loading' : ' loaded'
+ }`}
+ />
+
+
+ )}
+
+
+
+
+
+ {showScatterplotMatrix && (
+ <>
+ {
- setFormState({
- dependentVariable: selectedOptions[0].label || '',
- });
- }}
- isClearable={false}
- isInvalid={dependentVariable === ''}
- data-test-subj={`mlAnalyticsCreateJobWizardDependentVariableSelect${
- loadingDepVarOptions ? ' loading' : ' loaded'
- }`}
- />
-
-
- )}
-
-
-
-
-
- {showScatterplotMatrix && (
- <>
+ fullWidth
+ >
+
+
+
+
+
+
+ >
+ )}
+ {isJobTypeWithDepVar && (
-
+ setFormState({ trainingPercent: +e.target.value })}
+ data-test-subj="mlAnalyticsCreateJobWizardTrainingPercentSlider"
+ />
-
-
-
-
- >
- )}
- {isJobTypeWithDepVar && (
-
- setFormState({ trainingPercent: +e.target.value })}
- data-test-subj="mlAnalyticsCreateJobWizardTrainingPercentSlider"
- />
-
- )}
-
- {
- setCurrentStep(ANALYTICS_STEPS.ADVANCED);
- }}
- />
-
+ )}
+
+ {
+ setCurrentStep(ANALYTICS_STEPS.ADVANCED);
+ }}
+ />
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts
index 37526b6e66ff8..ab7cd1d6c1c59 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts
@@ -10,12 +10,13 @@ import {
buildEsQuery,
buildQueryFromFilters,
decorateQuery,
+ Filter,
fromKueryExpression,
+ Query,
toElasticsearchQuery,
} from '@kbn/es-query';
import { useMlContext } from '../../../../../contexts/ml';
import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search';
-import { getQueryFromSavedSearchObject } from '../../../../../util/index_utils';
// `undefined` is used for a non-initialized state
// `null` is set if no saved search is used
@@ -33,14 +34,16 @@ export function useSavedSearch() {
const [savedSearchQueryStr, setSavedSearchQueryStr] = useState(undefined);
const mlContext = useMlContext();
- const { currentSavedSearch, currentDataView, kibanaConfig } = mlContext;
+ const { currentDataView, kibanaConfig, selectedSavedSearch } = mlContext;
const getQueryData = () => {
let qry: any = {};
let qryString;
- if (currentSavedSearch !== null) {
- const { query, filter } = getQueryFromSavedSearchObject(currentSavedSearch);
+ if (selectedSavedSearch) {
+ // FIXME: Add support for AggregateQuery type #150091
+ const query = selectedSavedSearch.searchSource.getField('query') as Query;
+ const filter = (selectedSavedSearch.searchSource.getField('filter') ?? []) as Filter[];
const queryLanguage = query.language;
qryString = query.query;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
index ecdb7af9e1ddf..4f681f07ff69e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
@@ -12,6 +12,8 @@ import { EuiDataGridColumn } from '@elastic/eui';
import { CoreSetup } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/public';
+import { isPopulatedObject } from '@kbn/ml-is-populated-object';
+import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
import { isRuntimeMappings } from '../../../../../../common/util/runtime_field_utils';
import { RuntimeMappings } from '../../../../../../common/types/fields';
import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../../common/constants/field_histograms';
@@ -85,6 +87,8 @@ export const useIndexData = (
// (for example, as part of filebeat/metricbeat/ECS based indices)
// to the data grid component which would significantly slow down the page.
const [indexPatternFields, setIndexPatternFields] = useState();
+ const [timeRangeMs, setTimeRangeMs] = useState();
+
useEffect(() => {
async function fetchDataGridSampleDocuments() {
setErrorMessage('');
@@ -170,6 +174,7 @@ export const useIndexData = (
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);
+ const timeFieldName = indexPattern.getTimeField()?.name;
const sort: EsSorting = sortingColumns.reduce((s, column) => {
s[column.id] = { order: column.direction };
return s;
@@ -191,11 +196,38 @@ export const useIndexData = (
...(isRuntimeMappings(combinedRuntimeMappings)
? { runtime_mappings: combinedRuntimeMappings }
: {}),
+ ...(timeFieldName
+ ? {
+ aggs: {
+ earliest: {
+ min: {
+ field: timeFieldName,
+ },
+ },
+ latest: {
+ max: {
+ field: timeFieldName,
+ },
+ },
+ },
+ }
+ : {}),
},
};
try {
const resp: IndexSearchResponse = await ml.esSearch(esSearchRequest);
+
+ if (
+ resp.aggregations &&
+ isPopulatedObject(resp.aggregations.earliest, ['value']) &&
+ isPopulatedObject(resp.aggregations.latest, ['value'])
+ ) {
+ setTimeRangeMs({
+ from: resp.aggregations.earliest.value as number,
+ to: resp.aggregations.latest.value as number,
+ } as TimeRangeMs);
+ }
const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
setRowCountInfo({
@@ -269,5 +301,6 @@ export const useIndexData = (
...dataGrid,
indexPatternFields,
renderCellValue,
+ timeRangeMs,
};
};
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
index d2ce04f8beb7e..005b1a76aef88 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
@@ -16,7 +16,7 @@ import {
EuiHorizontalRule,
EuiTextArea,
} from '@elastic/eui';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
import { JobCreatorContext } from '../../../job_creator_context';
import { AdvancedJobCreator } from '../../../../../common/job_creator';
import {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx
index 7f2e1ec3ebed1..cd6c7894ddafb 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx
@@ -7,10 +7,10 @@
import React, { FC, useContext, useState, useEffect, useMemo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
-import { FieldStatsInfoButton } from '../../../../../common/components/field_stats_info_button';
+import { FieldStatsInfoButton } from '../../../../../../../components/field_stats_flyout/field_stats_info_button';
import { JobCreatorContext } from '../../../job_creator_context';
import { Field, Aggregation, AggFieldPair } from '../../../../../../../../../common/types/fields';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
// The display label used for an aggregation e.g. sum(bytes).
export type Label = string;
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
index a2992d1572b61..5e73c5e1b253b 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
@@ -8,7 +8,7 @@
import React, { FC, useCallback, useContext, useMemo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
import { JobCreatorContext } from '../../../job_creator_context';
import { Field } from '../../../../../../../../../common/types/fields';
import { createFieldOptions } from '../../../../../common/job_creator/util/general';
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx
index a63a49bc5f893..ffa4833138bc6 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx
@@ -8,7 +8,7 @@
import React, { FC, useCallback, useContext, useMemo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
import { JobCreatorContext } from '../../../job_creator_context';
import { Field } from '../../../../../../../../../common/types/fields';
import { createFieldOptions } from '../../../../../common/job_creator/util/general';
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx
index f132023f45765..6ea00a7123e72 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx
@@ -7,7 +7,7 @@
import React, { FC, useCallback, useMemo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
import { Field } from '../../../../../../../../../common/types/fields';
interface DropDownLabel {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
index 52e0b5c8c4752..5795fe9c0b2a7 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
@@ -8,7 +8,7 @@
import React, { FC, useContext } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
import { JobCreatorContext } from '../../../job_creator_context';
import { Field } from '../../../../../../../../../common/types/fields';
import {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx
index 8393fe111dc33..ae36daba25804 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx
@@ -8,7 +8,7 @@
import React, { FC } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
import { Field, SplitField } from '../../../../../../../../../common/types/fields';
interface DropDownLabel {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx
index ed5a0041508b1..23e676efcff34 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx
@@ -8,7 +8,7 @@
import React, { FC } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
import { Field, SplitField } from '../../../../../../../../../common/types/fields';
interface DropDownLabel {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
index d91f4539a831f..fa8ce4c3f22af 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
@@ -8,7 +8,7 @@
import React, { FC, useContext } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
-import { useFieldStatsTrigger } from '../../../../../utils/use_field_stats_trigger';
+import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger';
import { JobCreatorContext } from '../../../job_creator_context';
import { Field } from '../../../../../../../../../common/types/fields';
import {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx
index 7d25031dc766c..ec08e25bb6f15 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx
@@ -21,7 +21,6 @@ import { ES_FIELD_TYPES } from '@kbn/field-types';
import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana';
import { useMlContext } from '../../../../contexts/ml';
-import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana';
import { DataRecognizer } from '../../../../components/data_recognizer';
import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed';
import { timeBasedIndexCheck } from '../../../../util/index_utils';
@@ -46,7 +45,7 @@ export const Page: FC = () => {
const [recognizerResultsCount, setRecognizerResultsCount] = useState(0);
- const { currentSavedSearch, currentDataView } = mlContext;
+ const { currentDataView, selectedSavedSearch } = mlContext;
const isTimeBasedIndex = timeBasedIndexCheck(currentDataView);
const hasGeoFields = useMemo(
@@ -58,27 +57,27 @@ export const Page: FC = () => {
[currentDataView]
);
const indexWarningTitle =
- !isTimeBasedIndex && isSavedSearchSavedObject(currentSavedSearch)
+ !isTimeBasedIndex && selectedSavedSearch
? i18n.translate(
'xpack.ml.newJob.wizard.jobType.dataViewFromSavedSearchNotTimeBasedMessage',
{
defaultMessage:
'{savedSearchTitle} uses data view {dataViewName} which is not time based',
values: {
- savedSearchTitle: currentSavedSearch.attributes.title as string,
- dataViewName: currentDataView.title,
+ savedSearchTitle: selectedSavedSearch.title ?? '',
+ dataViewName: currentDataView.getName(),
},
}
)
: i18n.translate('xpack.ml.newJob.wizard.jobType.dataViewNotTimeBasedMessage', {
defaultMessage: 'Data view {dataViewName} is not time based',
- values: { dataViewName: currentDataView.title },
+ values: { dataViewName: currentDataView.getName() },
});
- const pageTitleLabel = isSavedSearchSavedObject(currentSavedSearch)
+ const pageTitleLabel = selectedSavedSearch
? i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', {
defaultMessage: 'saved search {savedSearchTitle}',
- values: { savedSearchTitle: currentSavedSearch.attributes.title as string },
+ values: { savedSearchTitle: selectedSavedSearch.title ?? '' },
})
: i18n.translate('xpack.ml.newJob.wizard.jobType.dataViewPageTitleLabel', {
defaultMessage: 'data view {dataViewName}',
@@ -93,23 +92,23 @@ export const Page: FC = () => {
};
const getUrlParams = () => {
- return !isSavedSearchSavedObject(currentSavedSearch)
+ return !selectedSavedSearch
? `?index=${currentDataView.id}`
- : `?savedSearchId=${currentSavedSearch.id}`;
+ : `?savedSearchId=${selectedSavedSearch.id}`;
};
const addSelectionToRecentlyAccessed = async () => {
- const title = !isSavedSearchSavedObject(currentSavedSearch)
- ? currentDataView.title
- : (currentSavedSearch.attributes.title as string);
+ const title = !selectedSavedSearch
+ ? currentDataView.getName()
+ : selectedSavedSearch.title ?? '';
const mlLocator = share.url.locators.get(ML_APP_LOCATOR)!;
const dataVisualizerLink = await mlLocator.getUrl(
{
page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER,
pageState: {
- ...(currentSavedSearch?.id
- ? { savedSearchId: currentSavedSearch.id }
+ ...(selectedSavedSearch?.id
+ ? { savedSearchId: selectedSavedSearch.id }
: { index: currentDataView.id }),
},
},
@@ -295,7 +294,7 @@ export const Page: FC = () => {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
index 4d27dba214242..57e0a6d75720e 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
@@ -64,7 +64,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => {
() =>
jobCreatorFactory(jobType)(
mlContext.currentDataView,
- mlContext.currentSavedSearch,
+ mlContext.deprecatedSavedSearchObj,
mlContext.combinedQuery
),
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -148,7 +148,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => {
jobCreator.modelChangeAnnotations = true;
}
- if (mlContext.currentSavedSearch !== null) {
+ if (mlContext.selectedSavedSearch !== null) {
// Jobs created from saved searches cannot be cloned in the wizard as the
// ML job config holds no reference to the saved search ID.
jobCreator.createdBy = null;
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx
index ba4ea850acf84..db8374f4bec44 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx
@@ -5,12 +5,15 @@
* 2.0.
*/
-import React, { Fragment, FC, useState } from 'react';
+import React, { Fragment, FC, useState, useMemo, useEffect, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { type FieldStatsServices } from '@kbn/unified-field-list-plugin/public';
+import { JobCreatorContext } from '../components/job_creator_context';
+import { useMlKibana } from '../../../../contexts/kibana';
import { FieldStatsFlyoutProvider } from '../../../../components/field_stats_flyout';
import { WIZARD_STEPS } from '../components/step_types';
@@ -30,20 +33,53 @@ interface Props {
export const WizardSteps: FC = ({ currentStep, setCurrentStep }) => {
const mlContext = useMlContext();
+ const { services } = useMlKibana();
+ const fieldStatsServices: FieldStatsServices = useMemo(() => {
+ const { uiSettings, data, fieldFormats, charts } = services;
+ return {
+ uiSettings,
+ dataViews: data.dataViews,
+ data,
+ fieldFormats,
+ charts,
+ };
+ }, [services]);
+
+ const { jobCreator, jobCreatorUpdated } = useContext(JobCreatorContext);
+
+ const [start, setStart] = useState(jobCreator?.start);
+ const [end, setEnd] = useState(jobCreator?.end);
+
+ useEffect(() => {
+ if ((jobCreator && jobCreator.start !== start) || jobCreator.end !== end) {
+ setStart(jobCreator.start);
+ setEnd(jobCreator.end);
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [jobCreatorUpdated]);
+
+ // Format timestamp to ISO formatted date strings
+ const timeRangeMs = useMemo(() => {
+ // If time range is available via jobCreator, use that
+ // else mimic Discover and set timeRange to be now for data view without time field
+ return start && end ? { from: start, to: start } : undefined;
+ }, [start, end]);
+
// store whether the advanced and additional sections have been expanded.
// has to be stored at this level to ensure it's remembered on wizard step change
const [advancedExpanded, setAdvancedExpanded] = useState(false);
const [additionalExpanded, setAdditionalExpanded] = useState(false);
function getSummaryStepTitle() {
- if (mlContext.currentSavedSearch !== null) {
+ if (mlContext.selectedSavedSearch) {
return i18n.translate('xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleSavedSearch', {
defaultMessage: 'New job from saved search {title}',
- values: { title: mlContext.currentSavedSearch.attributes.title as string },
+ values: { title: mlContext.selectedSavedSearch.title ?? '' },
});
} else if (mlContext.currentDataView.id !== undefined) {
return i18n.translate('xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleDataView', {
defaultMessage: 'New job from data view {dataViewName}',
- values: { dataViewName: mlContext.currentDataView.title },
+ values: { dataViewName: mlContext.currentDataView.getName() },
});
}
return '';
@@ -81,7 +117,12 @@ export const WizardSteps: FC = ({ currentStep, setCurrentStep }) => {
)}
{currentStep === WIZARD_STEPS.PICK_FIELDS && (
-
+
<>
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx
index 1dbf2b2e99a84..5881f76632e70 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx
@@ -92,23 +92,18 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => {
const [jobsAwaitingNodeCount, setJobsAwaitingNodeCount] = useState(0);
// #endregion
- const {
- currentSavedSearch: savedSearch,
- currentDataView: dataView,
- combinedQuery,
- } = useMlContext();
- const pageTitle =
- savedSearch !== null
- ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', {
- defaultMessage: 'saved search {savedSearchTitle}',
- values: { savedSearchTitle: savedSearch.attributes.title as string },
- })
- : i18n.translate('xpack.ml.newJob.recognize.dataViewPageTitle', {
- defaultMessage: 'data view {dataViewName}',
- values: { dataViewName: dataView.getName() },
- });
- const displayQueryWarning = savedSearch !== null;
- const tempQuery = savedSearch === null ? undefined : combinedQuery;
+ const { selectedSavedSearch, currentDataView: dataView, combinedQuery } = useMlContext();
+ const pageTitle = selectedSavedSearch
+ ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', {
+ defaultMessage: 'saved search {savedSearchTitle}',
+ values: { savedSearchTitle: selectedSavedSearch.title ?? '' },
+ })
+ : i18n.translate('xpack.ml.newJob.recognize.dataViewPageTitle', {
+ defaultMessage: 'data view {dataViewName}',
+ values: { dataViewName: dataView.getName() },
+ });
+ const displayQueryWarning = selectedSavedSearch !== null;
+ const tempQuery = selectedSavedSearch === null ? undefined : combinedQuery;
/**
* Loads recognizer module configuration.
diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx
index e050bea78a5c9..4b9dab76994ed 100644
--- a/x-pack/plugins/ml/public/application/routing/router.tsx
+++ b/x-pack/plugins/ml/public/application/routing/router.tsx
@@ -19,6 +19,8 @@ import type { DataViewsContract } from '@kbn/data-views-plugin/public';
import { EuiLoadingContent } from '@elastic/eui';
import { UrlStateProvider } from '@kbn/ml-url-state';
+import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
+import { SavedObjectsClientContract } from '@kbn/core/public';
import { MlNotificationsContextProvider } from '../contexts/ml/ml_notifications_context';
import { MlContext, MlContextValue } from '../contexts/ml';
@@ -65,6 +67,10 @@ export interface PageDependencies {
dataViewsContract: DataViewsContract;
setBreadcrumbs: ChromeStart['setBreadcrumbs'];
redirectToMlAccessDeniedPage: () => Promise;
+ getSavedSearchDeps: {
+ search: DataPublicPluginStart['search'];
+ savedObjectsClient: SavedObjectsClientContract;
+ };
}
export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => {
diff --git a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx
index 1868c521c72fd..d07f760adf791 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx
@@ -30,7 +30,14 @@ export const accessDeniedRouteFactory = (): MlRoute => ({
});
const PageWrapper: FC = ({ deps }) => {
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {});
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {}
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/aiops/change_point_detection.tsx b/x-pack/plugins/ml/public/application/routing/routes/aiops/change_point_detection.tsx
index 8d85c09f5ddf1..c825c579e6497 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/aiops/change_point_detection.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/aiops/change_point_detection.tsx
@@ -43,10 +43,17 @@ export const changePointDetectionRouteFactory = (
const PageWrapper: FC = ({ location, deps }) => {
const { index, savedSearchId }: Record = parse(location.search, { sort: false });
- const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, {
- checkBasicLicense,
- cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
- });
+ const { context } = useResolver(
+ index,
+ savedSearchId,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkBasicLicense,
+ cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
+ }
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx b/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx
index ac9728f0c54a0..b946f1a651099 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx
@@ -50,15 +50,23 @@ export const explainLogRateSpikesRouteFactory = (
disabled: !AIOPS_ENABLED,
});
-const PageWrapper: FC = ({ location, deps }) => {
+const PageWrapper: FC = ({ location, deps, ...restProps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { index, savedSearchId }: Record = parse(location.search, { sort: false });
- const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, {
- checkBasicLicense,
- cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- });
+ const { context } = useResolver(
+ index,
+ savedSearchId,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkBasicLicense,
+ cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ }
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/aiops/log_categorization.tsx b/x-pack/plugins/ml/public/application/routing/routes/aiops/log_categorization.tsx
index e3aebb87b5341..6002d86a69520 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/aiops/log_categorization.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/aiops/log_categorization.tsx
@@ -50,11 +50,19 @@ const PageWrapper: FC = ({ location, deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { index, savedSearchId }: Record = parse(location.search, { sort: false });
- const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, {
- checkBasicLicense,
- cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- });
+ const { context } = useResolver(
+ index,
+ savedSearchId,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkBasicLicense,
+ cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ }
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx
index fa0899ea23475..9466c271f31b0 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx
@@ -48,11 +48,18 @@ const PageWrapper: FC = ({ location, deps }) => {
sort: false,
});
- const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, {
- ...basicResolvers(deps),
- analyticsFields: () =>
- loadNewJobCapabilities(index, savedSearchId, deps.dataViewsContract, DATA_FRAME_ANALYTICS),
- });
+ const { context } = useResolver(
+ index,
+ savedSearchId,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ ...basicResolvers(deps),
+ analyticsFields: () =>
+ loadNewJobCapabilities(index, savedSearchId, deps.dataViewsContract, DATA_FRAME_ANALYTICS),
+ }
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
index d47780553eede..1d4e483126e7f 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
@@ -46,6 +46,7 @@ const PageWrapper: FC = ({ deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
basicResolvers(deps)
);
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx
index fd3a365d01179..0b94eb490c778 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx
@@ -44,6 +44,7 @@ const PageWrapper: FC = ({ location, deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
basicResolvers(deps)
);
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx
index faae919f722c8..853be56943341 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx
@@ -45,6 +45,7 @@ const PageWrapper: FC = ({ deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
basicResolvers(deps)
);
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_source_selection.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_source_selection.tsx
index e9ffcc509b153..6d255a8fb5fb3 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_source_selection.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_source_selection.tsx
@@ -44,6 +44,7 @@ const PageWrapper: FC = ({ deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
basicResolvers(deps)
);
diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx
index ad01c2d97b718..29f9063cddd38 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx
@@ -35,11 +35,18 @@ export const selectorRouteFactory = (
const PageWrapper: FC = ({ location, deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkBasicLicense,
- checkFindFileStructurePrivilege: () =>
- checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage),
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkBasicLicense,
+ checkFindFileStructurePrivilege: () =>
+ checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage),
+ }
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx
index 3d048e8d40283..3e58a2b2bd08f 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx
@@ -45,12 +45,19 @@ export const fileBasedRouteFactory = (
const PageWrapper: FC = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkBasicLicense,
- cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
- checkFindFileStructurePrivilege: () =>
- checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage),
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkBasicLicense,
+ cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
+ checkFindFileStructurePrivilege: () =>
+ checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage),
+ }
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx
index 1ba865435280c..acd51501092b8 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx
@@ -47,11 +47,19 @@ const PageWrapper: FC = ({ location, deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { index, savedSearchId }: Record = parse(location.search, { sort: false });
- const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, {
- checkBasicLicense,
- cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- });
+ const { context } = useResolver(
+ index,
+ savedSearchId,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkBasicLicense,
+ cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ }
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
index e7e672248f625..99c3f3ff38a68 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -75,6 +75,7 @@ const PageWrapper: FC = ({ deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
{
...basicResolvers(deps),
jobs: mlJobService.loadJobsWrapper,
diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx
index 68c0cdbe6a24f..bcf8929c7f470 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx
@@ -50,6 +50,7 @@ const PageWrapper: FC = ({ deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
basicResolvers(deps)
);
const timefilter = useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true });
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_lens.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_lens.tsx
index a0690b4987bc0..0a9d1a30aad62 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_lens.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_lens.tsx
@@ -30,9 +30,16 @@ const PageWrapper: FC = ({ location, deps }) => {
}
);
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- redirect: () => resolver(lensId, vis, from, to, query, filters, layerIndex),
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ redirect: () => resolver(lensId, vis, from, to, query, filters, layerIndex),
+ }
+ );
return (
{ }
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_map.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_map.tsx
index 3f1ebb04162e6..e63a00735b348 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_map.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_map.tsx
@@ -36,10 +36,17 @@ const PageWrapper: FC = ({ location, deps }) => {
sort: false,
});
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- redirect: () =>
- resolver(dashboard, dataViewId, embeddable, geoField, splitField, from, to, layer),
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ redirect: () =>
+ resolver(dashboard, dataViewId, embeddable, geoField, splitField, from, to, layer),
+ }
+ );
return (
{ }
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
index 0383b48eabadc..fa35731e567c7 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
@@ -206,6 +206,7 @@ const PageWrapper: FC = ({ nextStepPath, deps, mode }) =
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
mode === MODE.NEW_JOB ? newJobResolvers : dataVizResolvers
);
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx
index e1d38875bc1dd..e4c9fa21cbd46 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx
@@ -41,6 +41,7 @@ const PageWrapper: FC = ({ location, deps }) => {
savedSearchId,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
basicResolvers(deps)
);
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx
index a68bb0d861a5c..1c1c54aebc4cc 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx
@@ -55,6 +55,8 @@ const PageWrapper: FC = ({ location, deps }) => {
savedSearchId,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+
{
...basicResolvers(deps),
existingJobsAndGroups: mlJobService.getJobAndGroupIds,
@@ -77,7 +79,7 @@ const CheckViewOrCreateWrapper: FC = ({ location, deps }) => {
const navigateToPath = useNavigateToPath();
// the single resolver checkViewOrCreateJobs redirects only. so will always reject
- useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
+ useResolver(undefined, undefined, deps.config, deps.dataViewsContract, deps.getSavedSearchDeps, {
checkViewOrCreateJobs: () =>
checkViewOrCreateJobs(moduleId, dataViewId, createLinkWithUserDefaults, navigateToPath),
});
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx
index 8a4640a346a85..4300d73ae010e 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx
@@ -200,6 +200,7 @@ const PageWrapper: FC = ({ location, jobType, deps }) => {
savedSearchId,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
{
...basicResolvers(deps),
privileges: () => checkCreateJobsCapabilitiesResolver(redirectToJobsManagementPage),
diff --git a/x-pack/plugins/ml/public/application/routing/routes/notifications.tsx b/x-pack/plugins/ml/public/application/routing/routes/notifications.tsx
index cb7cbc0c48c6e..843cf54087c8f 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/notifications.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/notifications.tsx
@@ -46,12 +46,20 @@ export const notificationsRouteFactory = (
const PageWrapper: FC = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkFullLicense,
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- getMlNodeCount,
- loadMlServerInfo,
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkFullLicense,
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ getMlNodeCount,
+ loadMlServerInfo,
+ }
+ );
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx
index 139e4635ce412..111a1ae77916c 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx
@@ -50,12 +50,20 @@ export const overviewRouteFactory = (
const PageWrapper: FC = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkFullLicense,
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- getMlNodeCount,
- loadMlServerInfo,
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkFullLicense,
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ getMlNodeCount,
+ loadMlServerInfo,
+ }
+ );
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx
index e257f0cde5aac..2d43c5ab5675c 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx
@@ -41,11 +41,19 @@ export const calendarListRouteFactory = (
const PageWrapper: FC = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkFullLicense,
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- getMlNodeCount,
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkFullLicense,
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ getMlNodeCount,
+ }
+ );
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx
index 9753e790cd543..775ba25523a9d 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx
@@ -85,11 +85,19 @@ const PageWrapper: FC = ({ location, mode, deps }) => {
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
);
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkFullLicense,
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkFullLicense,
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
+ }
+ );
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx
index d4e8b61ef4870..abbe806716feb 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx
@@ -41,11 +41,19 @@ export const filterListRouteFactory = (
const PageWrapper: FC = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkFullLicense,
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- getMlNodeCount,
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkFullLicense,
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ getMlNodeCount,
+ }
+ );
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx
index c501760080624..9afa6ec8c8557 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx
@@ -89,11 +89,19 @@ const PageWrapper: FC = ({ location, mode, deps }) => {
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
);
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkFullLicense,
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkFullLicense,
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
+ }
+ );
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx
index 2e994cc4f85f9..2b9af849e2157 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx
@@ -41,11 +41,19 @@ export const settingsRouteFactory = (
const PageWrapper: FC = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
- const { context } = useResolver(undefined, undefined, deps.config, deps.dataViewsContract, {
- checkFullLicense,
- checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
- getMlNodeCount,
- });
+ const { context } = useResolver(
+ undefined,
+ undefined,
+ deps.config,
+ deps.dataViewsContract,
+ deps.getSavedSearchDeps,
+ {
+ checkFullLicense,
+ checkGetJobsCapabilities: () =>
+ checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ getMlNodeCount,
+ }
+ );
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
index e89a2cff13873..729bb5ae821d8 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
@@ -79,6 +79,7 @@ const PageWrapper: FC = ({ deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
{
...basicResolvers(deps),
jobs: mlJobService.loadJobsWrapper,
diff --git a/x-pack/plugins/ml/public/application/routing/routes/trained_models/models_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/trained_models/models_list.tsx
index e2b0b2709df52..46fd368829344 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/trained_models/models_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/trained_models/models_list.tsx
@@ -47,6 +47,7 @@ const PageWrapper: FC = ({ location, deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
basicResolvers(deps)
);
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/trained_models/nodes_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/trained_models/nodes_list.tsx
index e6003f161b1c6..78d99daf0d2d7 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/trained_models/nodes_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/trained_models/nodes_list.tsx
@@ -46,6 +46,7 @@ const PageWrapper: FC = ({ location, deps }) => {
undefined,
deps.config,
deps.dataViewsContract,
+ deps.getSavedSearchDeps,
basicResolvers(deps)
);
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true });
diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts
index 714df97142cca..a75448459be96 100644
--- a/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts
+++ b/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts
@@ -13,7 +13,7 @@ import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'
import { useNotifications } from '../contexts/kibana';
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
-import { useResolver } from './use_resolver';
+import { type GetSavedSearchPageDeps, useResolver } from './use_resolver';
jest.mock('../contexts/kibana/use_create_url', () => {
return {
@@ -47,7 +47,14 @@ describe('useResolver', () => {
it('should accept undefined as dataViewId and savedSearchId.', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
- useResolver(undefined, undefined, {} as IUiSettingsClient, {} as DataViewsContract, {})
+ useResolver(
+ undefined,
+ undefined,
+ {} as IUiSettingsClient,
+ {} as DataViewsContract,
+ {} as GetSavedSearchPageDeps,
+ {}
+ )
);
await act(async () => {
@@ -66,9 +73,10 @@ describe('useResolver', () => {
},
},
currentDataView: null,
- currentSavedSearch: null,
+ deprecatedSavedSearchObj: null,
dataViewsContract: {},
kibanaConfig: {},
+ selectedSavedSearch: null,
},
results: {},
});
@@ -78,7 +86,14 @@ describe('useResolver', () => {
it('should add an error toast and redirect if dataViewId is an empty string.', async () => {
const { result } = renderHook(() =>
- useResolver('', undefined, {} as IUiSettingsClient, {} as DataViewsContract, {})
+ useResolver(
+ '',
+ undefined,
+ {} as IUiSettingsClient,
+ {} as DataViewsContract,
+ {} as GetSavedSearchPageDeps,
+ {}
+ )
);
await act(async () => {});
diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.ts
index 6e16b1d42e6f6..0f999329b2fa1 100644
--- a/x-pack/plugins/ml/public/application/routing/use_resolver.ts
+++ b/x-pack/plugins/ml/public/application/routing/use_resolver.ts
@@ -6,21 +6,27 @@
*/
import { useEffect, useState } from 'react';
-import { IUiSettingsClient } from '@kbn/core/public';
+import type { IUiSettingsClient, SavedObjectsClientContract } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
+import { getSavedSearch } from '@kbn/saved-search-plugin/public';
+import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import {
getDataViewById,
getDataViewAndSavedSearch,
- DataViewAndSavedSearch,
+ type DataViewAndSavedSearch,
} from '../util/index_utils';
import { createSearchItems } from '../jobs/new_job/utils/new_job_utils';
-import { ResolverResults, Resolvers } from './resolvers';
-import { MlContextValue } from '../contexts/ml';
+import type { ResolverResults, Resolvers } from './resolvers';
+import type { MlContextValue } from '../contexts/ml';
import { useNotifications } from '../contexts/kibana';
import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url';
import { ML_PAGES } from '../../../common/constants/locator';
+export interface GetSavedSearchPageDeps {
+ search: DataPublicPluginStart['search'];
+ savedObjectsClient: SavedObjectsClientContract;
+}
/**
* Hook to resolve route specific requirements
* @param dataViewId optional Kibana data view id, used for wizards
@@ -34,6 +40,10 @@ export const useResolver = (
savedSearchId: string | undefined,
config: IUiSettingsClient,
dataViewsContract: DataViewsContract,
+ getSavedSearchDeps: {
+ search: DataPublicPluginStart['search'];
+ savedObjectsClient: SavedObjectsClientContract;
+ },
resolvers: Resolvers
): { context: MlContextValue; results: ResolverResults } => {
const notifications = useNotifications();
@@ -76,25 +86,28 @@ export const useResolver = (
savedSearch: null,
dataView: null,
};
+ let savedSearch = null;
if (savedSearchId !== undefined) {
+ savedSearch = await getSavedSearch(savedSearchId, getSavedSearchDeps);
dataViewAndSavedSearch = await getDataViewAndSavedSearch(savedSearchId);
} else if (dataViewId !== undefined) {
dataViewAndSavedSearch.dataView = await getDataViewById(dataViewId);
}
- const { savedSearch, dataView } = dataViewAndSavedSearch;
+ const { savedSearch: deprecatedSavedSearchObj, dataView } = dataViewAndSavedSearch;
const { combinedQuery } = createSearchItems(
config,
dataView !== null ? dataView : undefined,
- savedSearch
+ deprecatedSavedSearchObj
);
setContext({
combinedQuery,
currentDataView: dataView,
- currentSavedSearch: savedSearch,
+ deprecatedSavedSearchObj,
+ selectedSavedSearch: savedSearch,
dataViewsContract,
kibanaConfig: config,
});
diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts
index 7fb27f889c417..f247be1004718 100644
--- a/x-pack/plugins/ml/public/shared.ts
+++ b/x-pack/plugins/ml/public/shared.ts
@@ -21,4 +21,5 @@ export * from '../common/util/date_utils';
export * from './application/formatters/metric_change_description';
export * from './application/components/data_grid';
+export * from './application/components/field_stats_flyout';
export * from './application/data_frame_analytics/common';
diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json
index d613793099257..5dc4bfd67dc58 100644
--- a/x-pack/plugins/ml/tsconfig.json
+++ b/x-pack/plugins/ml/tsconfig.json
@@ -79,5 +79,6 @@
"@kbn/shared-ux-link-redirect-app",
"@kbn/react-field",
"@kbn/unified-field-list-plugin",
+ "@kbn/saved-search-plugin",
],
}
diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts
index 290aae563c37b..a76e09a2cd5e2 100644
--- a/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts
+++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts
@@ -18,7 +18,9 @@ import {
findAndClickButton,
findFormFieldByRowsLabelAndType,
inputQuery,
+ loadAlertsEvents,
submitQuery,
+ toggleRuleOffAndOn,
typeInECSFieldInput,
} from '../../tasks/live_query';
import { preparePack } from '../../tasks/packs';
@@ -61,17 +63,10 @@ describe('Alert Event Details', () => {
cy.contains(`Successfully updated "${PACK_NAME}" pack`);
cy.getBySel('toastCloseButton').click();
- cy.visit('/app/security/rules');
- cy.contains(RULE_NAME);
- cy.wait(2000);
- cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
- cy.getBySel('ruleSwitch').click();
- cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false');
- cy.getBySel('ruleSwitch').click();
- cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
+ toggleRuleOffAndOn(RULE_NAME);
});
- it('adds response actations with osquery with proper validation and form values', () => {
+ it('adds response actions with osquery with proper validation and form values', () => {
cy.visit('/app/security/rules');
cy.contains(RULE_NAME).click();
cy.contains('Edit rule settings').click();
@@ -225,27 +220,19 @@ describe('Alert Event Details', () => {
cy.contains('select * from uptime');
});
cy.getBySel(RESPONSE_ACTIONS_ITEM_2).within(() => {
- cy.contains('SELECT * FROM processes;');
+ cy.contains("SELECT * FROM os_version where name='{{host.os.name}}';");
});
cy.getBySel(RESPONSE_ACTIONS_ITEM_3).within(() => {
cy.contains('select * from users');
});
+ cy.contains('Save changes').click();
+ cy.contains(`${RULE_NAME} was saved`).should('exist');
+ cy.getBySel('toastCloseButton').click();
});
it('should be able to run live query and add to timeline (-depending on the previous test)', () => {
const TIMELINE_NAME = 'Untitled timeline';
- cy.visit('/app/security/alerts');
- cy.getBySel('header-page-title').contains('Alerts').should('exist');
- cy.getBySel('expand-event')
- .first()
- .within(() => {
- cy.get(`[data-is-loading="true"]`).should('exist');
- });
- cy.getBySel('expand-event')
- .first()
- .within(() => {
- cy.get(`[data-is-loading="true"]`).should('not.exist');
- });
+ loadAlertsEvents();
cy.getBySel('timeline-context-menu-button').first().click({ force: true });
cy.contains('Run Osquery');
cy.getBySel('expand-event').first().click({ force: true });
@@ -272,16 +259,41 @@ describe('Alert Event Details', () => {
cy.visit('/app/osquery');
closeModalIfVisible();
});
- // TODO think on how to get these actions triggered faster (because now they are not triggered during the test).
- // it.skip('sees osquery results from last action', () => {
- // cy.visit('/app/security/alerts');
- // cy.getBySel('header-page-title').contains('Alerts').should('exist');
- // cy.getBySel('expand-event').first().click({ force: true });
- // cy.contains('Osquery Results').click();
- // cy.getBySel('osquery-results').should('exist');
- // cy.contains('select * from uptime');
- // cy.getBySel('osqueryResultsTable').within(() => {
- // checkResults();
- // });
- // });
+
+ it('should substitute parameters in investigation guide', () => {
+ loadAlertsEvents();
+ cy.getBySel('expand-event').first().click({ force: true });
+ cy.contains('Get processes').click();
+ cy.contains("SELECT * FROM os_version where name='Ubuntu';");
+ });
+
+ it('sees osquery results from last action', () => {
+ toggleRuleOffAndOn(RULE_NAME);
+ cy.wait(2000);
+ cy.visit('/app/security/alerts');
+ cy.getBySel('header-page-title').contains('Alerts').should('exist');
+ cy.getBySel('expand-event').first().click({ force: true });
+ cy.contains('Osquery Results').click();
+ cy.getBySel('osquery-results').should('exist');
+ cy.contains('select * from uptime');
+ cy.contains('select * from users;');
+ cy.contains("SELECT * FROM os_version where name='Ubuntu';");
+ cy.getBySel('osquery-results-comment').each(($comment) => {
+ cy.wrap($comment).within(() => {
+ // On initial load result table might not render due to displayed error
+ if ($comment.find('div .euiDataGridRow').length <= 0) {
+ // If tabs are present try clicking between status and results to get rid of the error message
+ if ($comment.find('div .euiTabs').length > 0) {
+ cy.getBySel('osquery-status-tab').click();
+ cy.getBySel('osquery-results-tab').click();
+ cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0);
+ }
+ } else {
+ // Result tab was rendered successfully
+ cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0);
+ }
+ // }
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson
index d1804c3aafec6..0eec67de7ff2e 100644
--- a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson
+++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson
@@ -48,7 +48,7 @@
],
"query": "_id:*",
"filters": [],
- "note": "!{osquery{\"query\":\"SELECT * FROM processes;\",\"label\":\"Get processes\",\"ecs_mapping\":{\"process.pid\":{\"field\":\"pid\"},\"process.name\":{\"field\":\"name\"},\"process.executable\":{\"field\":\"path\"},\"process.args\":{\"field\":\"cmdline\"},\"process.working_directory\":{\"field\":\"cwd\"},\"user.id\":{\"field\":\"uid\"},\"group.id\":{\"field\":\"gid\"},\"process.parent.pid\":{\"field\":\"parent\"},\"process.pgid\":{\"field\":\"pgroup\"}}}}\n\n!{osquery{\"query\":\"select * from users;\",\"label\":\"Get users\"}}"
+ "note": "!{osquery{\"query\":\"SELECT * FROM os_version where name='{{host.os.name}}';\",\"label\":\"Get processes\",\"ecs_mapping\":{\"process.pid\":{\"field\":\"pid\"},\"process.name\":{\"field\":\"name\"},\"process.executable\":{\"field\":\"path\"},\"process.args\":{\"field\":\"cmdline\"},\"process.working_directory\":{\"field\":\"cwd\"},\"user.id\":{\"field\":\"uid\"},\"group.id\":{\"field\":\"gid\"},\"process.parent.pid\":{\"field\":\"parent\"},\"process.pgid\":{\"field\":\"pgroup\"}}}}\n\n!{osquery{\"query\":\"select * from users;\",\"label\":\"Get users\"}}"
},
"schedule": {
"interval": "5m"
@@ -94,4 +94,4 @@
"alert": "8.0.0"
},
"coreMigrationVersion": "8.1.0"
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts
index 0c2d789f4728a..00a855207999c 100644
--- a/x-pack/plugins/osquery/cypress/tasks/integrations.ts
+++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { DEFAULT_POLICY } from '../screens/fleet';
import {
ADD_POLICY_BTN,
CONFIRM_MODAL_BTN,
@@ -13,7 +14,7 @@ import {
DATA_COLLECTION_SETUP_STEP,
} from '../screens/integrations';
-export const addIntegration = (agentPolicy = 'Default Fleet Server policy') => {
+export const addIntegration = (agentPolicy = DEFAULT_POLICY) => {
cy.getBySel(ADD_POLICY_BTN).click();
cy.getBySel(DATA_COLLECTION_SETUP_STEP).find('.euiLoadingSpinner').should('not.exist');
cy.contains('Existing hosts').click();
diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts
index 519ebed2ce9a6..b9d9fe581b76d 100644
--- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts
+++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts
@@ -64,3 +64,29 @@ export const deleteAndConfirm = (type: string) => {
export const findAndClickButton = (text: string) => {
cy.react('EuiButton').contains(text).click();
};
+
+export const toggleRuleOffAndOn = (ruleName: string) => {
+ cy.visit('/app/security/rules');
+ cy.contains(ruleName);
+ cy.wait(2000);
+ cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
+ cy.getBySel('ruleSwitch').click();
+ cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false');
+ cy.getBySel('ruleSwitch').click();
+ cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
+};
+
+export const loadAlertsEvents = () => {
+ cy.visit('/app/security/alerts');
+ cy.getBySel('header-page-title').contains('Alerts').should('exist');
+ cy.getBySel('expand-event')
+ .first()
+ .within(() => {
+ cy.get(`[data-is-loading="true"]`).should('exist');
+ });
+ cy.getBySel('expand-event')
+ .first()
+ .within(() => {
+ cy.get(`[data-is-loading="true"]`).should('not.exist');
+ });
+};
diff --git a/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts b/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts
index 668b5b3f924df..14f31f29125e6 100644
--- a/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts
+++ b/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts
@@ -12,6 +12,7 @@ import { some, filter } from 'lodash';
import deepEqual from 'fast-deep-equal';
import type { ECSMappingOrUndefined } from '@kbn/osquery-io-ts-types';
+import { replaceParamsQuery } from '../../../common/utils/replace_params_query';
import { createLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query';
import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
@@ -66,9 +67,17 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp
osqueryQueries,
(payload: {
configuration: { query: string; ecs_mapping: ECSMappingOrUndefined };
- }) =>
- payload?.configuration?.query === request.body.query &&
- deepEqual(payload?.configuration?.ecs_mapping, request.body.ecs_mapping)
+ }) => {
+ const { result: replacedConfigurationQuery } = replaceParamsQuery(
+ payload.configuration.query,
+ alertData
+ );
+
+ return (
+ replacedConfigurationQuery === request.body.query &&
+ deepEqual(payload.configuration.ecs_mapping, request.body.ecs_mapping)
+ );
+ }
);
if (!requestQueryExistsInTheInvestigationGuide) throw new Error();
diff --git a/x-pack/plugins/security_solution/common/detection_engine/constants.ts b/x-pack/plugins/security_solution/common/detection_engine/constants.ts
index 6a6bf9df6d27a..bb3c1a063f56e 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/constants.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/constants.ts
@@ -25,3 +25,5 @@ export enum RULE_PREVIEW_FROM {
WEEK = 'now-65m',
MONTH = 'now-25h',
}
+
+export const PREBUILT_RULES_PACKAGE_NAME = 'security_detection_engine';
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.test.ts
index 2f11e1a9c120f..b2ff257ba2c4e 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.test.ts
@@ -24,6 +24,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -42,6 +46,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -60,6 +68,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [{ error: { status_code: 400, message: 'some message' } }],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -81,6 +93,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -102,6 +118,10 @@ describe('Import rules response schema', () => {
],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -120,6 +140,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -140,6 +164,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: -1,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -178,6 +206,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
@@ -217,6 +249,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: 'hello',
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
@@ -238,6 +274,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -262,6 +302,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -270,4 +314,182 @@ describe('Import rules response schema', () => {
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_data"']);
expect(message.schema).toEqual({});
});
+
+ test('it should validate an empty import response with a single connectors error', () => {
+ const payload: ImportRulesResponse = {
+ success: false,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [{ error: { status_code: 400, message: 'some message' } }],
+ action_connectors_warnings: [],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
+ test('it should validate an empty import response with multiple errors', () => {
+ const payload: ImportRulesResponse = {
+ success: false,
+ success_count: 0,
+ rules_count: 0,
+ errors: [
+ { error: { status_code: 400, message: 'some message' } },
+ { error: { status_code: 500, message: 'some message' } },
+ ],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [{ error: { status_code: 400, message: 'some message' } }],
+ action_connectors_warnings: [],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
+ test('it should NOT validate action_connectors_success that is not boolean', () => {
+ type UnsafeCastForTest = Either<
+ Errors,
+ {
+ success: boolean;
+ action_connectors_success: string;
+ success_count: number;
+ errors: Array<
+ {
+ id?: string | undefined;
+ rule_id?: string | undefined;
+ } & {
+ error: {
+ status_code: number;
+ message: string;
+ };
+ }
+ >;
+ }
+ >;
+ const payload: Omit & {
+ action_connectors_success: string;
+ } = {
+ success: true,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: 'invalid',
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded as UnsafeCastForTest);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "invalid" supplied to "action_connectors_success"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+ test('it should NOT validate a action_connectors_success_count that is a negative number', () => {
+ const payload: ImportRulesResponse = {
+ success: false,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: -1,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "-1" supplied to "action_connectors_success_count"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+ test('it should validate a action_connectors_warnings after importing successfully', () => {
+ const payload: ImportRulesResponse = {
+ success: false,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 1,
+ action_connectors_errors: [],
+ action_connectors_warnings: [{ type: 'type', message: 'message', actionPath: 'actionPath' }],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
+ test('it should NOT validate a action_connectors_warnings that is not WarningSchema', () => {
+ type UnsafeCastForTest = Either<
+ Errors,
+ {
+ success: boolean;
+ action_connectors_warnings: string;
+ success_count: number;
+ errors: Array<
+ {
+ id?: string | undefined;
+ rule_id?: string | undefined;
+ } & {
+ error: {
+ status_code: number;
+ message: string;
+ };
+ }
+ >;
+ }
+ >;
+ const payload: Omit & {
+ action_connectors_warnings: string;
+ } = {
+ success: true,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: 'invalid',
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded as UnsafeCastForTest);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "invalid" supplied to "action_connectors_warnings"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.ts
index 77ccd0812c2c9..99212b902c4d3 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.ts
@@ -7,7 +7,7 @@
import * as t from 'io-ts';
import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
-import { errorSchema } from '../../../../schemas/response/error_schema';
+import { errorSchema, warningSchema } from '../../../../schemas/response';
export type ImportRulesResponse = t.TypeOf;
export const ImportRulesResponse = t.exact(
@@ -19,5 +19,9 @@ export const ImportRulesResponse = t.exact(
success: t.boolean,
success_count: PositiveInteger,
errors: t.array(errorSchema),
+ action_connectors_errors: t.array(errorSchema),
+ action_connectors_warnings: t.array(warningSchema),
+ action_connectors_success: t.boolean,
+ action_connectors_success_count: PositiveInteger,
})
);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.mock.ts
index 64b82abdb3755..9a26b1a09f066 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.mock.ts
@@ -8,6 +8,7 @@
import { getExceptionExportDetailsMock } from '@kbn/lists-plugin/common/schemas/response/exception_export_details_schema.mock';
import type { ExportExceptionDetailsMock } from '@kbn/lists-plugin/common/schemas/response/exception_export_details_schema.mock';
import type { ExportRulesDetails } from './export_rules_details_schema';
+import type { DefaultActionConnectorDetails } from '../../../../../server/lib/detection_engine/rule_management/logic/export/get_export_rule_action_connectors';
interface RuleDetailsMock {
totalCount?: number;
@@ -16,6 +17,23 @@ interface RuleDetailsMock {
missingRules?: Array>;
}
+export const getActionConnectorDetailsMock = (): DefaultActionConnectorDetails => ({
+ exported_action_connector_count: 0,
+ missing_action_connection_count: 0,
+ missing_action_connections: [],
+ excluded_action_connection_count: 0,
+ excluded_action_connections: [],
+});
+export const getOutputDetailsSampleWithActionConnectors = (): ExportRulesDetails => ({
+ ...getOutputDetailsSample(),
+ ...getExceptionExportDetailsMock(),
+ exported_action_connector_count: 1,
+ missing_action_connection_count: 0,
+ missing_action_connections: [],
+ excluded_action_connection_count: 0,
+ excluded_action_connections: [],
+});
+
export const getOutputDetailsSample = (ruleDetails?: RuleDetailsMock): ExportRulesDetails => ({
exported_count: ruleDetails?.totalCount ?? 0,
exported_rules_count: ruleDetails?.rulesCount ?? 0,
@@ -29,6 +47,7 @@ export const getOutputDetailsSampleWithExceptions = (
): ExportRulesDetails => ({
...getOutputDetailsSample(ruleDetails),
...getExceptionExportDetailsMock(exceptionDetails),
+ ...getActionConnectorDetailsMock(),
});
export const getSampleDetailsAsNdjson = (sample: ExportRulesDetails): string => {
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.test.ts
index 2c84a34b34928..a91f59924ef72 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.test.ts
@@ -18,15 +18,16 @@ import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts
import {
getOutputDetailsSample,
+ getOutputDetailsSampleWithActionConnectors,
getOutputDetailsSampleWithExceptions,
} from './export_rules_details_schema.mock';
import type { ExportRulesDetails } from './export_rules_details_schema';
-import { exportRulesDetailsWithExceptionsSchema } from './export_rules_details_schema';
+import { exportRulesDetailsWithExceptionsAndConnectorsSchema } from './export_rules_details_schema';
-describe('exportRulesDetailsWithExceptionsSchema', () => {
+describe('exportRulesDetailsWithExceptionsAndConnectorsSchema', () => {
test('it should validate export details response', () => {
const payload = getOutputDetailsSample();
- const decoded = exportRulesDetailsWithExceptionsSchema.decode(payload);
+ const decoded = exportRulesDetailsWithExceptionsAndConnectorsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
@@ -36,7 +37,7 @@ describe('exportRulesDetailsWithExceptionsSchema', () => {
test('it should validate export details with exceptions details response', () => {
const payload = getOutputDetailsSampleWithExceptions();
- const decoded = exportRulesDetailsWithExceptionsSchema.decode(payload);
+ const decoded = exportRulesDetailsWithExceptionsAndConnectorsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
@@ -44,12 +45,21 @@ describe('exportRulesDetailsWithExceptionsSchema', () => {
expect(message.schema).toEqual(payload);
});
+ test('it should validate export details with action connectors details response', () => {
+ const payload = getOutputDetailsSampleWithActionConnectors();
+ const decoded = exportRulesDetailsWithExceptionsAndConnectorsSchema.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
test('it should strip out extra keys', () => {
const payload: ExportRulesDetails & {
extraKey?: string;
} = getOutputDetailsSample();
payload.extraKey = 'some extra key';
- const decoded = exportRulesDetailsWithExceptionsSchema.decode(payload);
+ const decoded = exportRulesDetailsWithExceptionsAndConnectorsSchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.ts
index 85b423135566b..205861bdd42a7 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.ts
@@ -9,13 +9,6 @@ import * as t from 'io-ts';
import { exportExceptionDetails } from '@kbn/securitysolution-io-ts-list-types';
import { NonEmptyString } from '@kbn/securitysolution-io-ts-types';
-const createSchema = (
- requiredFields: Required,
- optionalFields: Optional
-) => {
- return t.intersection([t.exact(t.type(requiredFields)), t.exact(t.partial(optionalFields))]);
-};
-
const exportRulesDetails = {
exported_count: t.number,
exported_rules_count: t.number,
@@ -28,11 +21,38 @@ const exportRulesDetails = {
),
missing_rules_count: t.number,
};
+const excludedActionConnectors = t.intersection([
+ t.exact(
+ t.type({
+ id: NonEmptyString,
+ type: NonEmptyString,
+ })
+ ),
+ t.exact(t.partial({ reason: t.string })),
+]);
+
+const exportRuleActionConnectorsDetails = {
+ exported_action_connector_count: t.number,
+ missing_action_connection_count: t.number,
+ missing_action_connections: t.array(
+ t.exact(
+ t.type({
+ id: NonEmptyString,
+ type: NonEmptyString,
+ })
+ )
+ ),
+ excluded_action_connection_count: t.number,
+ excluded_action_connections: t.array(excludedActionConnectors),
+};
-// With exceptions
-export const exportRulesDetailsWithExceptionsSchema = createSchema(
- exportRulesDetails,
- exportExceptionDetails
-);
+// With exceptions and connectors
+export const exportRulesDetailsWithExceptionsAndConnectorsSchema = t.intersection([
+ t.exact(t.type(exportRulesDetails)),
+ t.exact(t.partial(exportExceptionDetails)),
+ t.exact(t.partial(exportRuleActionConnectorsDetails)),
+]);
-export type ExportRulesDetails = t.TypeOf;
+export type ExportRulesDetails = t.TypeOf<
+ typeof exportRulesDetailsWithExceptionsAndConnectorsSchema
+>;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/import/rule_to_import.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/import/rule_to_import.mock.ts
index d1dc9e8ac4663..cec58d1c5fdc6 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/import/rule_to_import.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/import/rule_to_import.mock.ts
@@ -80,3 +80,41 @@ export const getImportThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): RuleToIm
},
],
});
+
+export const webHookConnector = {
+ id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
+ type: 'action',
+ updated_at: '2023-01-25T14:35:52.852Z',
+ created_at: '2023-01-25T14:35:52.852Z',
+ version: 'WzUxNTksMV0=',
+ attributes: {
+ actionTypeId: '.webhook',
+ name: 'webhook',
+ isMissingSecrets: false,
+ config: {},
+ secrets: {},
+ },
+ references: [],
+ migrationVersion: { action: '8.3.0' },
+ coreMigrationVersion: '8.7.0',
+};
+
+export const ruleWithConnectorNdJSON = (): string => {
+ const items = [
+ {
+ ...getImportRulesSchemaMock(),
+ actions: [
+ {
+ group: 'default',
+ id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
+ action_type_id: '.webhook',
+ params: {},
+ },
+ ],
+ },
+ webHookConnector,
+ ];
+ const stringOfExceptions = items.map((item) => JSON.stringify(item));
+
+ return stringOfExceptions.join('\n');
+};
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts
index b20a956525e2e..76da687604028 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts
@@ -6,3 +6,4 @@
*/
export * from './error_schema';
+export * from './warning_schema';
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/warning_schema/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/warning_schema/index.ts
new file mode 100644
index 0000000000000..1a401d1941cb0
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/warning_schema/index.ts
@@ -0,0 +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.
+ */
+
+import * as t from 'io-ts';
+
+const partial = t.exact(
+ t.partial({
+ buttonLabel: t.string,
+ })
+);
+const required = t.exact(
+ t.type({
+ type: t.string,
+ message: t.string,
+ actionPath: t.string,
+ })
+);
+
+export const warningSchema = t.intersection([partial, required]);
+export type WarningSchema = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts
index f74be26963d45..2012d756d6354 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts
@@ -85,9 +85,7 @@ describe('attach timeline to case', () => {
it('modal can be re-opened once closed', function () {
visitTimeline(this.timelineId);
attachTimelineToExistingCase();
- cy.get('[data-test-subj="all-cases-modal"] .euiButton')
- .contains('Cancel')
- .click({ force: true });
+ cy.get('[data-test-subj="all-cases-modal-cancel-button"]').click({ force: true });
cy.get('[data-test-subj="all-cases-modal"]').should('not.exist');
attachTimelineToExistingCase();
diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts
index 20f41f938502b..a80c7099c31ff 100644
--- a/x-pack/plugins/security_solution/cypress/objects/rule.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts
@@ -573,6 +573,11 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response {
.pipe(($el) => $el.trigger('click'))
.should('be.checked');
};
-
+const selectOverwriteConnectorsRulesImport = () => {
+ cy.get(RULE_IMPORT_OVERWRITE_CONNECTORS_CHECKBOX)
+ .pipe(($el) => $el.trigger('click'))
+ .should('be.checked');
+};
export const importRulesWithOverwriteAll = (rulesFile: string) => {
cy.get(RULE_IMPORT_MODAL).click();
cy.get(INPUT_FILE).should('exist');
cy.get(INPUT_FILE).trigger('click', { force: true }).attachFile(rulesFile).trigger('change');
selectOverwriteRulesImport();
selectOverwriteExceptionsRulesImport();
+ selectOverwriteConnectorsRulesImport();
cy.get(RULE_IMPORT_MODAL_BUTTON).last().click({ force: true });
cy.get(INPUT_FILE).should('not.exist');
};
diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
index 47dd897f71ffb..3378225f6b602 100644
--- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
@@ -90,13 +90,521 @@ Object {
+
+
+
+
+
+ checkBoxLabel
+
+
+
+
+
+
+
+
+
+
+
+
+
+