diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 381fad404ca73..9a09ea1de6943 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,12 +26,12 @@ /src/plugins/kibana_legacy/ @elastic/kibana-vis-editors /src/plugins/timelion/ @elastic/kibana-vis-editors /src/plugins/vis_default_editor/ @elastic/kibana-vis-editors -/src/plugins/vis_type_metric/ @elastic/kibana-vis-editors +/src/plugins/vis_types/metric/ @elastic/kibana-vis-editors /src/plugins/vis_type_table/ @elastic/kibana-vis-editors -/src/plugins/vis_type_tagcloud/ @elastic/kibana-vis-editors +/src/plugins/vis_types/tagcloud/ @elastic/kibana-vis-editors /src/plugins/vis_type_timelion/ @elastic/kibana-vis-editors /src/plugins/vis_type_timeseries/ @elastic/kibana-vis-editors -/src/plugins/vis_type_vega/ @elastic/kibana-vis-editors +/src/plugins/vis_types/vega/ @elastic/kibana-vis-editors /src/plugins/vis_types/vislib/ @elastic/kibana-vis-editors /src/plugins/vis_types/xy/ @elastic/kibana-vis-editors /src/plugins/vis_types/pie/ @elastic/kibana-vis-editors diff --git a/.i18nrc.json b/.i18nrc.json index f38d6b8faae7e..77c57ded8242b 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -60,11 +60,11 @@ "uiActions": "src/plugins/ui_actions", "visDefaultEditor": "src/plugins/vis_default_editor", "visTypeMarkdown": "src/plugins/vis_type_markdown", - "visTypeMetric": "src/plugins/vis_type_metric", + "visTypeMetric": "src/plugins/vis_types/metric", "visTypeTable": "src/plugins/vis_type_table", - "visTypeTagCloud": "src/plugins/vis_type_tagcloud", + "visTypeTagCloud": "src/plugins/vis_types/tagcloud", "visTypeTimeseries": "src/plugins/vis_type_timeseries", - "visTypeVega": "src/plugins/vis_type_vega", + "visTypeVega": "src/plugins/vis_types/vega", "visTypeVislib": "src/plugins/vis_types/vislib", "visTypeXy": "src/plugins/vis_types/xy", "visTypePie": "src/plugins/vis_types/pie", diff --git a/api_docs/core.json b/api_docs/core.json index 8edb5d3b7ce63..93a84eb38f5c6 100644 --- a/api_docs/core.json +++ b/api_docs/core.json @@ -1125,7 +1125,7 @@ "references": [ { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/plugin.ts" + "path": "src/plugins/vis_types/vega/public/plugin.ts" } ] }, @@ -1423,15 +1423,15 @@ }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/data_model/search_api.ts" + "path": "src/plugins/vis_types/vega/public/data_model/search_api.ts" }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/plugin.ts" + "path": "src/plugins/vis_types/vega/public/plugin.ts" }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/target/types/public/data_model/search_api.d.ts" + "path": "src/plugins/vis_types/vega/target/types/public/data_model/search_api.d.ts" } ] } diff --git a/api_docs/data.json b/api_docs/data.json index d0cbb6851a8fe..d76adbc36a7c4 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -16157,11 +16157,11 @@ }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" }, { "plugin": "dashboard", @@ -20282,15 +20282,15 @@ }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" } ], "children": [ @@ -22396,7 +22396,7 @@ }, { "plugin": "visTypeMetric", - "path": "src/plugins/vis_type_metric/public/plugin.ts" + "path": "src/plugins/vis_types/metric/public/plugin.ts" }, { "plugin": "visTypeTable", @@ -30417,11 +30417,11 @@ }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" }, { "plugin": "dashboard", @@ -36898,11 +36898,11 @@ }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" }, { "plugin": "visTypeVega", - "path": "src/plugins/vis_type_vega/public/vega_request_handler.ts" + "path": "src/plugins/vis_types/vega/public/vega_request_handler.ts" }, { "plugin": "dashboard", diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 24cfe1e5342a7..de62579ee05dc 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -679,7 +679,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_metric/public/plugin.ts#:~:text=fieldFormats) | - | +| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/metric/public/plugin.ts#:~:text=fieldFormats) | - | @@ -738,12 +738,12 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=esQuery), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=esQuery), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=esQuery) | 8.1 | -| | [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=Filter), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=Filter) | 8.1 | -| | [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=Filter), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=Filter) | 8.1 | -| | [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=Filter), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/vega_request_handler.ts#:~:text=Filter) | 8.1 | -| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/plugin.ts#:~:text=injectedMetadata) | - | -| | [search_api.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/data_model/search_api.ts#:~:text=injectedMetadata), [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/public/plugin.ts#:~:text=injectedMetadata), [search_api.d.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_vega/target/types/public/data_model/search_api.d.ts#:~:text=injectedMetadata) | - | +| | [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=esQuery), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=esQuery), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=esQuery) | 8.1 | +| | [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=Filter), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=Filter) | 8.1 | +| | [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=Filter), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=Filter) | 8.1 | +| | [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=Filter), [vega_request_handler.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/vega_request_handler.ts#:~:text=Filter) | 8.1 | +| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/plugin.ts#:~:text=injectedMetadata) | - | +| | [search_api.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/data_model/search_api.ts#:~:text=injectedMetadata), [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/plugin.ts#:~:text=injectedMetadata), [search_api.d.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/target/types/public/data_model/search_api.d.ts#:~:text=injectedMetadata) | - | diff --git a/api_docs/vis_type_vega.json b/api_docs/vis_type_vega.json index 88a5bda07a2f2..1ecbc7c48f289 100644 --- a/api_docs/vis_type_vega.json +++ b/api_docs/vis_type_vega.json @@ -22,7 +22,7 @@ "tags": [], "label": "VisTypeVegaPluginStart", "description": [], - "path": "src/plugins/vis_type_vega/server/types.ts", + "path": "src/plugins/vis_types/vega/server/types.ts", "deprecated": false, "children": [], "lifecycle": "start", @@ -35,7 +35,7 @@ "tags": [], "label": "VisTypeVegaPluginSetup", "description": [], - "path": "src/plugins/vis_type_vega/server/types.ts", + "path": "src/plugins/vis_types/vega/server/types.ts", "deprecated": false, "children": [], "lifecycle": "setup", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e9925014d5a71..d2d543ff59d59 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -286,7 +286,7 @@ The plugin exposes the static DefaultEditorController class to consume. |The markdown visualization that can be used to place text panels on dashboards. -|{kib-repo}blob/{branch}/src/plugins/vis_type_metric[visTypeMetric] +|{kib-repo}blob/{branch}/src/plugins/vis_types/metric[visTypeMetric] |WARNING: Missing README. @@ -298,7 +298,7 @@ The plugin exposes the static DefaultEditorController class to consume. |Contains the data table visualization, that allows presenting data in a simple table format. -|{kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud[visTypeTagcloud] +|{kib-repo}blob/{branch}/src/plugins/vis_types/tagcloud[visTypeTagcloud] |WARNING: Missing README. @@ -310,7 +310,7 @@ The plugin exposes the static DefaultEditorController class to consume. |WARNING: Missing README. -|{kib-repo}blob/{branch}/src/plugins/vis_type_vega[visTypeVega] +|{kib-repo}blob/{branch}/src/plugins/vis_types/vega[visTypeVega] |WARNING: Missing README. diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md index 75732f59f1b3f..eb0dbb59e6c12 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md @@ -31,10 +31,10 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations // Example of a manual correctiveAction deprecations.push({ title: i18n.translate('xpack.timelion.deprecations.worksheetsTitle', { - defaultMessage: 'Found Timelion worksheets.' + defaultMessage: 'Timelion worksheets are deprecated' }), message: i18n.translate('xpack.timelion.deprecations.worksheetsMessage', { - defaultMessage: 'You have {count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.', + defaultMessage: 'You have {count} Timelion worksheets. Migrate your Timelion worksheets to a dashboard to continue using them.', values: { count }, }), documentationUrl: diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index bab04b8052674..98d7b2591a572 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -51,7 +51,7 @@ Use the <> to customize connecto Config defines information for the connector type. -`service`:: The name of a https://nodemailer.com/smtp/well-known/[well-known email service provider]. If `service` is provided, `host`, `port`, and `secure` properties are ignored. For more information on the `gmail` service value, see the https://nodemailer.com/usage/using-gmail/[Nodemailer Gmail documentation]. +`service`:: The name of the email service. If `service` is `elastic_cloud` (for Elastic Cloud notifications) or one of Nodemailer's https://nodemailer.com/smtp/well-known/[well-known email service providers], `host`, `port`, and `secure` properties are ignored. If `service` is `other`, `host` and `port` properties must be defined. For more information on the `gmail` service value, see the https://nodemailer.com/usage/using-gmail/[Nodemailer Gmail documentation]. `from`:: An email address that corresponds to *Sender*. `host`:: A string that corresponds to *Host*. `port`:: A number that corresponds to *Port*. diff --git a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc index c57e9876a4118..408b18143f27f 100644 --- a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc +++ b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc @@ -68,7 +68,7 @@ Rules are taking a long time to execute and are impacting the overall health of [IMPORTANT] ============================================== -By default, only users with a `superuser` role can query the {kib} event log because it is a system index. To enable additional users to execute this query, assign `read` privileges to the `.kibana-event-log*` index. +By default, only users with a `superuser` role can query the experimental[] {kib} event log because it is a system index. To enable additional users to execute this query, assign `read` privileges to the `.kibana-event-log*` index. ============================================== *Solution* diff --git a/docs/user/alerting/troubleshooting/event-log-index.asciidoc b/docs/user/alerting/troubleshooting/event-log-index.asciidoc index fa5b5831c04ee..393b982b279f5 100644 --- a/docs/user/alerting/troubleshooting/event-log-index.asciidoc +++ b/docs/user/alerting/troubleshooting/event-log-index.asciidoc @@ -2,6 +2,8 @@ [[event-log-index]] === Event log index +experimental[] + Use the event log index to determine: * Whether a rule successfully ran but its associated actions did not diff --git a/docs/user/production-considerations/alerting-production-considerations.asciidoc b/docs/user/production-considerations/alerting-production-considerations.asciidoc index 57cc2a72a8895..cd8a60a1d5fe3 100644 --- a/docs/user/production-considerations/alerting-production-considerations.asciidoc +++ b/docs/user/production-considerations/alerting-production-considerations.asciidoc @@ -54,6 +54,8 @@ Predicting the buffer required to account for actions depends heavily on the rul [[event-log-ilm]] === Event log index lifecycle managment +experimental[] + Alerts and actions log activity in a set of "event log" indices. These indices are configured with an index lifecycle management (ILM) policy, which you can customize. The default policy rolls over the index when it reaches 50GB, or after 30 days. Indices over 90 days old are deleted. The name of the index policy is `kibana-event-log-policy`. {kib} creates the index policy on startup, if it doesn't already exist. The index policy can be customized for your environment, but {kib} never modifies the index policy after creating it. diff --git a/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts b/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts index c730091c1478b..25c3d7e156e91 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts @@ -8,26 +8,14 @@ import { KbnClientStatus } from './kbn_client_status'; -const PLUGIN_STATUS_ID = /^plugin:(.+?)@/; - export class KbnClientPlugins { constructor(private readonly status: KbnClientStatus) {} /** * Get a list of plugin ids that are enabled on the server */ public async getEnabledIds() { - const pluginIds: string[] = []; const apiResp = await this.status.get(); - for (const status of apiResp.status.statuses) { - if (status.id) { - const match = status.id.match(PLUGIN_STATUS_ID); - if (match) { - pluginIds.push(match[1]); - } - } - } - - return pluginIds; + return Object.keys(apiResp.status.plugins); } } diff --git a/packages/kbn-test/src/kbn_client/kbn_client_status.ts b/packages/kbn-test/src/kbn_client/kbn_client_status.ts index 26c46917ae8dd..ed08b6b8cea15 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_status.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_status.ts @@ -9,13 +9,11 @@ import { KbnClientRequester } from './kbn_client_requester'; interface Status { - state: 'green' | 'red' | 'yellow'; - title?: string; - id?: string; - icon: string; - message: string; - uiColor: string; - since: string; + level: 'available' | 'degraded' | 'unavailable' | 'critical'; + summary: string; + detail?: string; + documentationUrl?: string; + meta?: Record; } interface ApiResponseStatus { @@ -29,7 +27,8 @@ interface ApiResponseStatus { }; status: { overall: Status; - statuses: Status[]; + core: Record; + plugins: Record; }; metrics: unknown; } @@ -55,6 +54,6 @@ export class KbnClientStatus { */ public async getOverallState() { const status = await this.get(); - return status.status.overall.state; + return status.status.overall.level; } } diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index aa6e1831f5e71..c541b9faaeccc 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -20,6 +20,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/api_integration/config.js'), require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/examples/config.js'), + require.resolve('../test/functional_execution_context/config.ts'), ]; require('../src/setup_node_env'); diff --git a/src/core/public/core_app/status/lib/load_status.ts b/src/core/public/core_app/status/lib/load_status.ts index a5cc18ffd6c16..e65764771f0fc 100644 --- a/src/core/public/core_app/status/lib/load_status.ts +++ b/src/core/public/core_app/status/lib/load_status.ts @@ -145,7 +145,7 @@ export async function loadStatus({ let response: StatusResponse; try { - response = await http.get('/api/status', { query: { v8format: true } }); + response = await http.get('/api/status'); } catch (e) { // API returns a 503 response if not all services are available. // In this case, we want to treat this as a successful API call, so that we can diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index 941ac5afacb40..331a3bbb9c028 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -10,12 +10,14 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import { CoreUsageDataService } from './core_usage_data_service'; import { coreUsageStatsClientMock } from './core_usage_stats_client.mock'; -import { CoreUsageData, CoreUsageDataSetup, CoreUsageDataStart } from './types'; +import { CoreUsageData, InternalCoreUsageDataSetup, CoreUsageDataStart } from './types'; const createSetupContractMock = (usageStatsClient = coreUsageStatsClientMock.create()) => { - const setupContract: jest.Mocked = { + const setupContract: jest.Mocked = { registerType: jest.fn(), getClient: jest.fn().mockReturnValue(usageStatsClient), + registerUsageCounter: jest.fn(), + incrementUsageCounter: jest.fn(), }; return setupContract; }; diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 478cfe5daff46..3c05069d3cd07 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -150,6 +150,50 @@ describe('CoreUsageDataService', () => { expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient); }); }); + + describe('Usage Counter', () => { + it('registers a usage counter and uses it to increment the counters', async () => { + const http = httpServiceMock.createInternalSetupContract(); + const metrics = metricsServiceMock.createInternalSetupContract(); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); + const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$(); + const coreUsageData = service.setup({ + http, + metrics, + savedObjectsStartPromise, + changedDeprecatedConfigPath$, + }); + const myUsageCounter = { incrementCounter: jest.fn() }; + coreUsageData.registerUsageCounter(myUsageCounter); + coreUsageData.incrementUsageCounter({ counterName: 'test' }); + expect(myUsageCounter.incrementCounter).toHaveBeenCalledWith({ counterName: 'test' }); + }); + + it('swallows errors when provided increment counter fails', async () => { + const http = httpServiceMock.createInternalSetupContract(); + const metrics = metricsServiceMock.createInternalSetupContract(); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); + const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$(); + const coreUsageData = service.setup({ + http, + metrics, + savedObjectsStartPromise, + changedDeprecatedConfigPath$, + }); + const myUsageCounter = { + incrementCounter: jest.fn(() => { + throw new Error('Something is really wrong'); + }), + }; + coreUsageData.registerUsageCounter(myUsageCounter); + expect(() => coreUsageData.incrementUsageCounter({ counterName: 'test' })).not.toThrow(); + expect(myUsageCounter.incrementCounter).toHaveBeenCalledWith({ counterName: 'test' }); + }); + }); }); describe('start', () => { diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 73f63d4d634df..ce9013d9437d6 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -27,7 +27,7 @@ import type { CoreServicesUsageData, CoreUsageData, CoreUsageDataStart, - CoreUsageDataSetup, + InternalCoreUsageDataSetup, ConfigUsageData, CoreConfigUsageData, } from './types'; @@ -39,6 +39,7 @@ import { LEGACY_URL_ALIAS_TYPE } from '../saved_objects/object_types'; import { CORE_USAGE_STATS_TYPE } from './constants'; import { CoreUsageStatsClient } from './core_usage_stats_client'; import { MetricsServiceSetup, OpsMetrics } from '..'; +import { CoreIncrementUsageCounter } from './types'; export type ExposedConfigsToUsage = Map>; @@ -86,7 +87,8 @@ const isCustomIndex = (index: string) => { return index !== '.kibana'; }; -export class CoreUsageDataService implements CoreService { +export class CoreUsageDataService + implements CoreService { private logger: Logger; private elasticsearchConfig?: ElasticsearchConfigType; private configService: CoreContext['configService']; @@ -98,6 +100,7 @@ export class CoreUsageDataService implements CoreService {}; // Initially set to noop constructor(core: CoreContext) { this.logger = core.logger.get('core-usage-stats-service'); @@ -495,7 +498,24 @@ export class CoreUsageDataService implements CoreService { + this.incrementUsageCounter = (params) => usageCounter.incrementCounter(params); + }, + incrementUsageCounter: (params) => { + try { + this.incrementUsageCounter(params); + } catch (e) { + // Self-defense mechanism since the handler is externally registered + this.logger.debug('Failed to increase the usage counter'); + this.logger.debug(e); + } + }, + }; + + return contract; } start({ savedObjects, elasticsearch, exposedConfigsToUsage }: StartDeps) { diff --git a/src/core/server/core_usage_data/index.ts b/src/core/server/core_usage_data/index.ts index a5c62c75f62d5..4687446bdb3a3 100644 --- a/src/core/server/core_usage_data/index.ts +++ b/src/core/server/core_usage_data/index.ts @@ -7,7 +7,15 @@ */ export { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants'; -export type { CoreUsageDataSetup, ConfigUsageData, CoreUsageDataStart } from './types'; +export type { + InternalCoreUsageDataSetup, + ConfigUsageData, + CoreUsageDataStart, + CoreUsageDataSetup, + CoreUsageCounter, + CoreIncrementUsageCounter, + CoreIncrementCounterParams, +} from './types'; export { CoreUsageDataService } from './core_usage_data_service'; export { CoreUsageStatsClient, REPOSITORY_RESOLVE_OUTCOME_STATS } from './core_usage_stats_client'; diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index 563a2a337cc8d..68e0b56c56db4 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -280,12 +280,59 @@ export interface CoreConfigUsageData { }; } +/** + * @internal Details about the counter to be incremented + */ +export interface CoreIncrementCounterParams { + /** The name of the counter **/ + counterName: string; + /** The counter type ("count" by default) **/ + counterType?: string; + /** Increment the counter by this number (1 if not specified) **/ + incrementBy?: number; +} + +/** + * @internal + * Method to call whenever an event occurs, so the counter can be increased. + */ +export type CoreIncrementUsageCounter = (params: CoreIncrementCounterParams) => void; + +/** + * @internal + * API to track whenever an event occurs, so the core can report them. + */ +export interface CoreUsageCounter { + /** @internal {@link CoreIncrementUsageCounter} **/ + incrementCounter: CoreIncrementUsageCounter; +} + /** @internal */ -export interface CoreUsageDataSetup { +export interface InternalCoreUsageDataSetup extends CoreUsageDataSetup { registerType( typeRegistry: ISavedObjectTypeRegistry & Pick ): void; getClient(): CoreUsageStatsClient; + + /** @internal {@link CoreIncrementUsageCounter} **/ + incrementUsageCounter: CoreIncrementUsageCounter; +} + +/** + * Internal API for registering the Usage Tracker used for Core's usage data payload. + * + * @note This API should never be used to drive application logic and is only + * intended for telemetry purposes. + * + * @internal + */ +export interface CoreUsageDataSetup { + /** + * @internal + * API for a usage tracker plugin to inject the {@link CoreUsageCounter} to use + * when tracking events. + */ + registerUsageCounter: (usageCounter: CoreUsageCounter) => void; } /** diff --git a/src/core/server/elasticsearch/integration_tests/client.test.ts b/src/core/server/elasticsearch/integration_tests/client.test.ts index 6e40c638614bd..83b20761df1ae 100644 --- a/src/core/server/elasticsearch/integration_tests/client.test.ts +++ b/src/core/server/elasticsearch/integration_tests/client.test.ts @@ -96,8 +96,8 @@ describe('fake elasticsearch', () => { test('should return unknown product when it cannot perform the Product check (503 response)', async () => { const resp = await supertest(kibanaHttpServer).get('/api/status').expect(503); - expect(resp.body.status.overall.state).toBe('red'); - expect(resp.body.status.statuses[0].message).toBe( + expect(resp.body.status.overall.level).toBe('critical'); + expect(resp.body.status.core.elasticsearch.summary).toBe( 'Unable to retrieve version information from Elasticsearch nodes. The client noticed that the server is not Elasticsearch and we do not support this unknown product.' ); }); diff --git a/src/core/server/execution_context/execution_context_service.test.ts b/src/core/server/execution_context/execution_context_service.test.ts index 3fa4de34ebda0..9bb76ad78c49a 100644 --- a/src/core/server/execution_context/execution_context_service.test.ts +++ b/src/core/server/execution_context/execution_context_service.test.ts @@ -109,7 +109,7 @@ describe('ExecutionContextService', () => { expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` Array [ Array [ - "set the execution context: {\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", + "{\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", ], ] `); @@ -351,7 +351,7 @@ describe('ExecutionContextService', () => { expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` Array [ Array [ - "stored the execution context: {\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", + "{\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", ], ] `); diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts index 41b225cf1d0f3..d8ff5fca7847d 100644 --- a/src/core/server/execution_context/execution_context_service.ts +++ b/src/core/server/execution_context/execution_context_service.ts @@ -124,7 +124,7 @@ export class ExecutionContextService // we have to use enterWith since Hapi lifecycle model is built on event emitters. // therefore if we wrapped request handler in asyncLocalStorage.run(), we would lose context in other lifecycles. this.contextStore.enterWith(contextContainer); - this.log.debug(`set the execution context: ${JSON.stringify(contextContainer)}`); + this.log.debug(JSON.stringify(contextContainer)); } private withContext( @@ -136,7 +136,7 @@ export class ExecutionContextService } const parent = this.contextStore.getStore(); const contextContainer = new ExecutionContextContainer(context, parent); - this.log.debug(`stored the execution context: ${JSON.stringify(contextContainer)}`); + this.log.debug(JSON.stringify(contextContainer)); return this.contextStore.run(contextContainer, fn); } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 1c3a0850d3b79..3a55d70109b8c 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -55,7 +55,7 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; -import { CoreUsageDataStart } from './core_usage_data'; +import { CoreUsageDataStart, CoreUsageDataSetup } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; import { DeprecationsServiceSetup, DeprecationsClient } from './deprecations'; // Because of #79265 we need to explicitly import, then export these types for @@ -410,7 +410,13 @@ export type { export { ServiceStatusLevels } from './status'; export type { CoreStatus, ServiceStatus, ServiceStatusLevel, StatusServiceSetup } from './status'; -export type { CoreUsageDataStart } from './core_usage_data'; +export type { + CoreUsageDataSetup, + CoreUsageDataStart, + CoreUsageCounter, + CoreIncrementUsageCounter, + CoreIncrementCounterParams, +} from './core_usage_data'; /** * Plugin specific context passed to a route handler. @@ -500,6 +506,8 @@ export interface CoreSetup; + /** @internal {@link CoreUsageDataSetup} */ + coreUsageData: CoreUsageDataSetup; } /** diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 8fc76e8b95743..29187c3963add 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -36,7 +36,7 @@ import { InternalRenderingServiceSetup } from './rendering'; import { InternalHttpResourcesPreboot, InternalHttpResourcesSetup } from './http_resources'; import { InternalStatusServiceSetup } from './status'; import { InternalLoggingServicePreboot, InternalLoggingServiceSetup } from './logging'; -import { CoreUsageDataStart } from './core_usage_data'; +import { CoreUsageDataStart, InternalCoreUsageDataSetup } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; import { InternalDeprecationsServiceSetup, InternalDeprecationsServiceStart } from './deprecations'; import type { @@ -73,6 +73,7 @@ export interface InternalCoreSetup { logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; deprecations: InternalDeprecationsServiceSetup; + coreUsageData: InternalCoreUsageDataSetup; } /** diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index b53658b574939..f8b56e81ab188 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -169,6 +169,9 @@ function createCoreSetupMock({ metrics: metricsServiceMock.createSetupContract(), deprecations: deprecationsServiceMock.createSetupContract(), executionContext: executionContextServiceMock.createInternalSetupContract(), + coreUsageData: { + registerUsageCounter: coreUsageDataServiceMock.createSetupContract().registerUsageCounter, + }, getStartServices: jest .fn, object, any]>, []>() .mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]), @@ -222,6 +225,7 @@ function createInternalCoreSetupMock() { metrics: metricsServiceMock.createInternalSetupContract(), deprecations: deprecationsServiceMock.createInternalSetupContract(), executionContext: executionContextServiceMock.createInternalSetupContract(), + coreUsageData: coreUsageDataServiceMock.createSetupContract(), }; return setupDeps; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index cbefdae525180..bdb4efde9b1fb 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -211,6 +211,9 @@ export function createPluginSetupContext( }, getStartServices: () => plugin.startDependencies, deprecations: deps.deprecations.getRegistry(plugin.name), + coreUsageData: { + registerUsageCounter: deps.coreUsageData.registerUsageCounter, + }, }; } diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 344a0d151cfb9..f8438a70d0418 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/bulk_get.ts b/src/core/server/saved_objects/routes/bulk_get.ts index cf051d6cd25cc..cffa69b06f4e4 100644 --- a/src/core/server/saved_objects/routes/bulk_get.ts +++ b/src/core/server/saved_objects/routes/bulk_get.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index de47ab9c59611..277673971dabe 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index 2fa7acfb6cab6..0e321aa7031f2 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts index fe08acf23fd23..e8404ba7fc8cf 100644 --- a/src/core/server/saved_objects/routes/delete.ts +++ b/src/core/server/saved_objects/routes/delete.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index e0293a4522fc1..e224f30a1bb02 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -11,7 +11,7 @@ import stringify from 'json-stable-stringify'; import { createPromiseFromStreams, createMapStream, createConcatStream } from '@kbn/utils'; import { IRouter, KibanaRequest } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsExportByTypeOptions, @@ -22,7 +22,7 @@ import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './util interface RouteDependencies { config: SavedObjectConfig; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } type EitherExportOptions = SavedObjectsExportByTypeOptions | SavedObjectsExportByObjectOptions; diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index d21039db30e5f..6e009f80bda7d 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/get.ts b/src/core/server/saved_objects/routes/get.ts index f28822d95d814..ae0656599a1e2 100644 --- a/src/core/server/saved_objects/routes/get.ts +++ b/src/core/server/saved_objects/routes/get.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 6f75bcf9fd5bf..d373dd5e63bc6 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -10,14 +10,14 @@ import { Readable } from 'stream'; import { extname } from 'path'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } interface FileStream extends Readable { diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 0c189113db096..3ab870c276d29 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -7,7 +7,7 @@ */ import { InternalHttpServiceSetup } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { Logger } from '../../logging'; import { SavedObjectConfig } from '../saved_objects_config'; import { IKibanaMigrator } from '../migrations'; @@ -33,7 +33,7 @@ export function registerRoutes({ migratorPromise, }: { http: InternalHttpServiceSetup; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; logger: Logger; config: SavedObjectConfig; migratorPromise: Promise; diff --git a/src/core/server/saved_objects/routes/resolve.ts b/src/core/server/saved_objects/routes/resolve.ts index ba409f7db7b67..78e85d17fe1fa 100644 --- a/src/core/server/saved_objects/routes/resolve.ts +++ b/src/core/server/saved_objects/routes/resolve.ts @@ -8,10 +8,10 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerResolveRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index a05c7d30b91fd..f1fe2e9cfe431 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -11,13 +11,13 @@ import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; import { chain } from 'lodash'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } interface FileStream extends Readable { diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts index b6dd9dc8e9ace..f21fc183cdade 100644 --- a/src/core/server/saved_objects/routes/update.ts +++ b/src/core/server/saved_objects/routes/update.ts @@ -8,12 +8,12 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import type { SavedObjectsUpdateOptions } from '../service/saved_objects_client'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b25e51da3a749..074eae55acaea 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -16,7 +16,7 @@ import { } from './'; import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; -import { CoreUsageDataSetup } from '../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../core_usage_data'; import { ElasticsearchClient, InternalElasticsearchServiceSetup, @@ -250,7 +250,7 @@ export interface SavedObjectsRepositoryFactory { export interface SavedObjectsSetupDeps { http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } interface WrappedClientFactoryWrapper { diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 3c178f6304425..0d7365c4b97c1 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -197,7 +197,16 @@ describe('SavedObjectsRepository', () => { { type, id, references, namespace: objectNamespace, originId }, namespace ) => { - const namespaceId = objectNamespace === 'default' ? undefined : objectNamespace ?? namespace; + let namespaces; + if (objectNamespace) { + namespaces = [objectNamespace]; + } else if (namespace) { + namespaces = Array.isArray(namespace) ? namespace : [namespace]; + } else { + namespaces = ['default']; + } + const namespaceId = namespaces[0] === 'default' ? undefined : namespaces[0]; + return { // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these found: true, @@ -207,7 +216,7 @@ describe('SavedObjectsRepository', () => { ...mockVersionProps, _source: { ...(registry.isSingleNamespace(type) && { namespace: namespaceId }), - ...(registry.isMultiNamespace(type) && { namespaces: [namespaceId ?? 'default'] }), + ...(registry.isMultiNamespace(type) && { namespaces }), ...(originId && { originId }), type, [type]: { title: 'Testing' }, @@ -219,7 +228,9 @@ describe('SavedObjectsRepository', () => { }; const getMockMgetResponse = (objects, namespace) => ({ - docs: objects.map((obj) => (obj.found === false ? obj : getMockGetResponse(obj, namespace))), + docs: objects.map((obj) => + obj.found === false ? obj : getMockGetResponse(obj, obj.initialNamespaces ?? namespace) + ), }); expect.extend({ @@ -797,6 +808,54 @@ describe('SavedObjectsRepository', () => { }); }); + it(`returns error when there is an unresolvable conflict with an existing multi-namespace saved object when using initialNamespaces (get)`, async () => { + const obj = { + ...obj3, + type: MULTI_NAMESPACE_TYPE, + initialNamespaces: ['foo-namespace', 'default'], + }; + const response1 = { + status: 200, + docs: [ + { + found: true, + _source: { + type: obj.type, + namespaces: ['bar-namespace'], + }, + }, + ], + }; + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response1) + ); + const response2 = getMockBulkCreateResponse([obj1, obj2]); + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response2) + ); + + const options = { overwrite: true }; + const result = await savedObjectsRepository.bulkCreate([obj1, obj, obj2], options); + + expect(client.bulk).toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalled(); + + const body1 = { docs: [expect.objectContaining({ _id: `${obj.type}:${obj.id}` })] }; + expect(client.mget).toHaveBeenCalledWith( + expect.objectContaining({ body: body1 }), + expect.anything() + ); + const body2 = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body: body2 }), + expect.anything() + ); + const expectedError = expectErrorConflict(obj, { metadata: { isNotOverwritable: true } }); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + }); + }); + it(`returns bulk error`, async () => { const expectedErrorResult = { type: obj3.type, id: obj3.id, error: 'Oh no, a bulk error!' }; await bulkCreateError(obj3, true, expectedErrorResult); @@ -2197,6 +2256,22 @@ describe('SavedObjectsRepository', () => { expect(client.get).toHaveBeenCalled(); }); + it(`throws when there is an unresolvable conflict with an existing multi-namespace saved object when using initialNamespaces (get)`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expect( + savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { + id, + overwrite: true, + initialNamespaces: ['bar-ns', 'dolly-ns'], + namespace, + }) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); + expect(client.get).toHaveBeenCalled(); + }); + it.todo(`throws when automatic index creation fails`); it.todo(`throws when an unexpected failure occurs`); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 9d9eafc0aad7f..e49b2e413981f 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -308,8 +308,12 @@ export class SavedObjectsRepository { if (id && overwrite) { // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces // note: this check throws an error if the object is found but does not exist in this namespace - const existingNamespaces = await this.preflightGetNamespaces(type, id, namespace); - savedObjectNamespaces = initialNamespaces || existingNamespaces; + savedObjectNamespaces = await this.preflightGetNamespaces( + type, + id, + namespace, + initialNamespaces + ); } else { savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); } @@ -455,8 +459,14 @@ export class SavedObjectsRepository { const indexFound = bulkGetResponse?.statusCode !== 404; const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined; const docFound = indexFound && actualResult?.found === true; - // @ts-expect-error MultiGetHit._source is optional - if (docFound && !this.rawDocExistsInNamespace(actualResult!, namespace)) { + if ( + docFound && + !this.rawDocExistsInNamespaces( + // @ts-expect-error MultiGetHit._source is optional + actualResult!, + initialNamespaces ?? [SavedObjectsUtils.namespaceIdToString(namespace)] + ) + ) { const { id, type } = object; return { tag: 'Left' as 'Left', @@ -2140,12 +2150,18 @@ export class SavedObjectsRepository { * @param type The type of the saved object. * @param id The ID of the saved object. * @param namespace The target namespace. + * @param initialNamespaces The target namespace(s) we intend to create the object in, if specified. * @returns Array of namespaces that this saved object currently includes, or (if the object does not exist yet) the namespaces that a * newly-created object will include. Value may be undefined if an existing saved object has no namespaces attribute; this should not * happen in normal operations, but it is possible if the Elasticsearch document is manually modified. * @throws Will throw an error if the saved object exists and it does not include the target namespace. */ - private async preflightGetNamespaces(type: string, id: string, namespace?: string) { + private async preflightGetNamespaces( + type: string, + id: string, + namespace: string | undefined, + initialNamespaces?: string[] + ) { if (!this._registry.isMultiNamespace(type)) { throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); } @@ -2160,17 +2176,19 @@ export class SavedObjectsRepository { } ); + const namespaces = initialNamespaces ?? [SavedObjectsUtils.namespaceIdToString(namespace)]; + const indexFound = statusCode !== 404; if (indexFound && isFoundGetResponse(body)) { - if (!this.rawDocExistsInNamespace(body, namespace)) { + if (!this.rawDocExistsInNamespaces(body, namespaces)) { throw SavedObjectsErrorHelpers.createConflictError(type, id); } - return getSavedObjectNamespaces(namespace, body); + return initialNamespaces ?? getSavedObjectNamespaces(namespace, body); } else if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { // checking if the 404 is from Elasticsearch throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); } - return getSavedObjectNamespaces(namespace); + return initialNamespaces ?? getSavedObjectNamespaces(namespace); } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index aa421fe393059..5abd1171a1936 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -359,6 +359,16 @@ export interface CoreEnvironmentUsageData { // @internal (undocumented) export type CoreId = symbol; +// @internal +export interface CoreIncrementCounterParams { + counterName: string; + counterType?: string; + incrementBy?: number; +} + +// @internal +export type CoreIncrementUsageCounter = (params: CoreIncrementCounterParams) => void; + // @public export interface CorePreboot { // (undocumented) @@ -395,6 +405,8 @@ export interface CoreSetup void; +} + // @internal export interface CoreUsageDataStart { // (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 27c35031db46f..865cc71a7e26b 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -243,6 +243,7 @@ export class Server { environment: environmentSetup, http: httpSetup, metrics: metricsSetup, + coreUsageData: coreUsageDataSetup, }); const renderingSetup = await this.rendering.setup({ @@ -278,6 +279,7 @@ export class Server { logging: loggingSetup, metrics: metricsSetup, deprecations: deprecationsSetup, + coreUsageData: coreUsageDataSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); diff --git a/src/core/server/status/routes/integration_tests/status.test.ts b/src/core/server/status/routes/integration_tests/status.test.ts index 645ce0b241612..082be62f8dc09 100644 --- a/src/core/server/status/routes/integration_tests/status.test.ts +++ b/src/core/server/status/routes/integration_tests/status.test.ts @@ -29,6 +29,7 @@ describe('GET /api/status', () => { let server: HttpService; let httpSetup: InternalHttpServiceSetup; let metrics: jest.Mocked; + let incrementUsageCounter: jest.Mock; const setupServer = async ({ allowAnonymous = true }: { allowAnonymous?: boolean } = {}) => { const coreContext = createCoreContext({ coreId }); @@ -50,6 +51,8 @@ describe('GET /api/status', () => { d: { level: ServiceStatusLevels.critical, summary: 'd is critical' }, }); + incrementUsageCounter = jest.fn(); + const router = httpSetup.createRouter(''); registerStatusRoute({ router, @@ -71,6 +74,7 @@ describe('GET /api/status', () => { core$: status.core$, plugins$: pluginsStatus$, }, + incrementUsageCounter, }); // Register dummy auth provider for testing auth @@ -137,69 +141,75 @@ describe('GET /api/status', () => { }); describe('legacy status format', () => { - it('returns legacy status format when no query params provided', async () => { - await setupServer(); - const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200); - expect(result.body.status).toEqual({ - overall: { + const legacyFormat = { + overall: { + icon: 'success', + nickname: 'Looking good', + since: expect.any(String), + state: 'green', + title: 'Green', + uiColor: 'secondary', + }, + statuses: [ + { + icon: 'success', + id: 'core:elasticsearch@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { icon: 'success', - nickname: 'Looking good', + id: 'core:savedObjects@9.9.9', + message: 'Service is working', since: expect.any(String), state: 'green', - title: 'Green', uiColor: 'secondary', }, - statuses: [ - { - icon: 'success', - id: 'core:elasticsearch@9.9.9', - message: 'Service is working', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'success', - id: 'core:savedObjects@9.9.9', - message: 'Service is working', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'success', - id: 'plugin:a@9.9.9', - message: 'a is available', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'warning', - id: 'plugin:b@9.9.9', - message: 'b is degraded', - since: expect.any(String), - state: 'yellow', - uiColor: 'warning', - }, - { - icon: 'danger', - id: 'plugin:c@9.9.9', - message: 'c is unavailable', - since: expect.any(String), - state: 'red', - uiColor: 'danger', - }, - { - icon: 'danger', - id: 'plugin:d@9.9.9', - message: 'd is critical', - since: expect.any(String), - state: 'red', - uiColor: 'danger', - }, - ], - }); + { + icon: 'success', + id: 'plugin:a@9.9.9', + message: 'a is available', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'warning', + id: 'plugin:b@9.9.9', + message: 'b is degraded', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + { + icon: 'danger', + id: 'plugin:c@9.9.9', + message: 'c is unavailable', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'danger', + id: 'plugin:d@9.9.9', + message: 'd is critical', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + ], + }; + + it('returns legacy status format when v7format=true is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v7format=true') + .expect(200); + expect(result.body.status).toEqual(legacyFormat); + expect(incrementUsageCounter).toHaveBeenCalledTimes(1); + expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' }); }); it('returns legacy status format when v8format=false is provided', async () => { @@ -207,109 +217,105 @@ describe('GET /api/status', () => { const result = await supertest(httpSetup.server.listener) .get('/api/status?v8format=false') .expect(200); - expect(result.body.status).toEqual({ - overall: { - icon: 'success', - nickname: 'Looking good', - since: expect.any(String), - state: 'green', - title: 'Green', - uiColor: 'secondary', - }, - statuses: [ - { - icon: 'success', - id: 'core:elasticsearch@9.9.9', - message: 'Service is working', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'success', - id: 'core:savedObjects@9.9.9', - message: 'Service is working', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'success', - id: 'plugin:a@9.9.9', - message: 'a is available', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'warning', - id: 'plugin:b@9.9.9', - message: 'b is degraded', - since: expect.any(String), - state: 'yellow', - uiColor: 'warning', - }, - { - icon: 'danger', - id: 'plugin:c@9.9.9', - message: 'c is unavailable', - since: expect.any(String), - state: 'red', - uiColor: 'danger', - }, - { - icon: 'danger', - id: 'plugin:d@9.9.9', - message: 'd is critical', - since: expect.any(String), - state: 'red', - uiColor: 'danger', - }, - ], - }); + expect(result.body.status).toEqual(legacyFormat); + expect(incrementUsageCounter).toHaveBeenCalledTimes(1); + expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' }); }); }); describe('v8format', () => { - it('returns new status format when v8format=true is provided', async () => { - await setupServer(); - const result = await supertest(httpSetup.server.listener) - .get('/api/status?v8format=true') - .expect(200); - expect(result.body.status).toEqual({ - core: { - elasticsearch: { - level: 'available', - summary: 'Service is working', - }, - savedObjects: { - level: 'available', - summary: 'Service is working', - }, + const newFormat = { + core: { + elasticsearch: { + level: 'available', + summary: 'Service is working', }, - overall: { + savedObjects: { level: 'available', summary: 'Service is working', }, - plugins: { - a: { - level: 'available', - summary: 'a is available', - }, - b: { - level: 'degraded', - summary: 'b is degraded', - }, - c: { - level: 'unavailable', - summary: 'c is unavailable', - }, - d: { - level: 'critical', - summary: 'd is critical', - }, + }, + overall: { + level: 'available', + summary: 'Service is working', + }, + plugins: { + a: { + level: 'available', + summary: 'a is available', + }, + b: { + level: 'degraded', + summary: 'b is degraded', + }, + c: { + level: 'unavailable', + summary: 'c is unavailable', }, - }); + d: { + level: 'critical', + summary: 'd is critical', + }, + }, + }; + + it('returns new status format when no query params are provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200); + expect(result.body.status).toEqual(newFormat); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('returns new status format when v8format=true is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v8format=true') + .expect(200); + expect(result.body.status).toEqual(newFormat); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('returns new status format when v7format=false is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v7format=false') + .expect(200); + expect(result.body.status).toEqual(newFormat); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + }); + + describe('invalid query parameters', () => { + it('v8format=true and v7format=true', async () => { + await setupServer(); + await supertest(httpSetup.server.listener) + .get('/api/status?v8format=true&v7format=true') + .expect(400); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('v8format=true and v7format=false', async () => { + await setupServer(); + await supertest(httpSetup.server.listener) + .get('/api/status?v8format=true&v7format=false') + .expect(400); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('v8format=false and v7format=false', async () => { + await setupServer(); + await supertest(httpSetup.server.listener) + .get('/api/status?v8format=false&v7format=false') + .expect(400); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('v8format=false and v7format=true', async () => { + await setupServer(); + await supertest(httpSetup.server.listener) + .get('/api/status?v8format=false&v7format=true') + .expect(400); + expect(incrementUsageCounter).not.toHaveBeenCalled(); }); }); }); diff --git a/src/core/server/status/routes/status.ts b/src/core/server/status/routes/status.ts index 43a596bd1e0ec..861b41c58a893 100644 --- a/src/core/server/status/routes/status.ts +++ b/src/core/server/status/routes/status.ts @@ -12,6 +12,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { MetricsServiceSetup } from '../../metrics'; +import type { CoreIncrementUsageCounter } from '../../core_usage_data/types'; import { ServiceStatus, CoreStatus, ServiceStatusLevels } from '../types'; import { PluginName } from '../../plugins'; import { calculateLegacyStatus, LegacyStatusInfo } from '../legacy_status'; @@ -34,6 +35,7 @@ interface Deps { core$: Observable; plugins$: Observable>; }; + incrementUsageCounter: CoreIncrementUsageCounter; } interface StatusInfo { @@ -47,7 +49,13 @@ interface StatusHttpBody extends Omit { status: StatusInfo | LegacyStatusInfo; } -export const registerStatusRoute = ({ router, config, metrics, status }: Deps) => { +export const registerStatusRoute = ({ + router, + config, + metrics, + status, + incrementUsageCounter, +}: Deps) => { // Since the status.plugins$ observable is not subscribed to elsewhere, we need to subscribe it here to eagerly load // the plugins status when Kibana starts up so this endpoint responds quickly on first boot. const combinedStatus$ = new ReplaySubject< @@ -63,9 +71,19 @@ export const registerStatusRoute = ({ router, config, metrics, status }: Deps) = tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page }, validate: { - query: schema.object({ - v8format: schema.boolean({ defaultValue: false }), - }), + query: schema.object( + { + v7format: schema.maybe(schema.boolean()), + v8format: schema.maybe(schema.boolean()), + }, + { + validate: ({ v7format, v8format }) => { + if (typeof v7format === 'boolean' && typeof v8format === 'boolean') { + return `provide only one format option: v7format or v8format`; + } + }, + } + ), }, }, async (context, req, res) => { @@ -73,14 +91,17 @@ export const registerStatusRoute = ({ router, config, metrics, status }: Deps) = const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, ''); const [overall, core, plugins] = await combinedStatus$.pipe(first()).toPromise(); + const { v8format = true, v7format = false } = req.query ?? {}; + let statusInfo: StatusInfo | LegacyStatusInfo; - if (req.query?.v8format) { + if (!v7format && v8format) { statusInfo = { overall, core, plugins, }; } else { + incrementUsageCounter({ counterName: 'status_v7format' }); statusInfo = calculateLegacyStatus({ overall, core, diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 4ead81a6638dd..9148f69e079aa 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -18,6 +18,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { mockRouter, RouterMock } from '../http/router/router.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { configServiceMock } from '../config/mocks'; +import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock'; expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); @@ -51,6 +52,7 @@ describe('StatusService', () => { environment: environmentServiceMock.createSetupContract(), http: httpServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), + coreUsageData: coreUsageDataServiceMock.createSetupContract(), ...overrides, }; }; diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 8e9db30bbebd3..107074bdb98b1 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -20,6 +20,7 @@ import { PluginName } from '../plugins'; import { InternalMetricsServiceSetup } from '../metrics'; import { registerStatusRoute } from './routes'; import { InternalEnvironmentServiceSetup } from '../environment'; +import type { InternalCoreUsageDataSetup } from '../core_usage_data'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; @@ -38,6 +39,7 @@ interface SetupDeps { http: InternalHttpServiceSetup; metrics: InternalMetricsServiceSetup; savedObjects: Pick; + coreUsageData: Pick; } export class StatusService implements CoreService { @@ -61,6 +63,7 @@ export class StatusService implements CoreService { metrics, savedObjects, environment, + coreUsageData, }: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); @@ -101,6 +104,7 @@ export class StatusService implements CoreService { plugins$: this.pluginsStatus.getAll$(), core$, }, + incrementUsageCounter: coreUsageData.incrementUsageCounter, }; const router = http.createRouter(''); diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 0af087f1427d7..4bb89e1b7e606 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -383,6 +383,7 @@ kibana_vars=( xpack.security.session.lifespan xpack.security.sessionTimeout xpack.securitySolution.alertMergeStrategy + xpack.securitySolution.alertIgnoreFields xpack.securitySolution.endpointResultListDefaultFirstPageIndex xpack.securitySolution.endpointResultListDefaultPageSize xpack.securitySolution.maxRuleImportExportSize diff --git a/src/plugins/charts/public/services/palettes/helpers.ts b/src/plugins/charts/public/services/palettes/helpers.ts index d4b1e98f94cc8..bd1f8350ba9f3 100644 --- a/src/plugins/charts/public/services/palettes/helpers.ts +++ b/src/plugins/charts/public/services/palettes/helpers.ts @@ -70,7 +70,10 @@ export function workoutColorForValue( const comparisonFn = (v: number, threshold: number) => v - threshold; // if steps are defined consider the specific rangeMax/Min as data boundaries - const maxRange = stops.length ? rangeMax : dataRangeArguments[1]; + // as of max reduce its value by 1/colors.length for correct continuity checks + const maxRange = stops.length + ? rangeMax + : dataRangeArguments[1] - (dataRangeArguments[1] - dataRangeArguments[0]) / colors.length; const minRange = stops.length ? rangeMin : dataRangeArguments[0]; // in case of shorter rangers, extends the steps on the sides to cover the whole set diff --git a/src/plugins/kibana_usage_collection/server/plugin.test.ts b/src/plugins/kibana_usage_collection/server/plugin.test.ts index 1584366a42dc1..75c323ba0332f 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.test.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.test.ts @@ -42,6 +42,8 @@ describe('kibana_usage_collection', () => { expect(pluginInstance.setup(coreSetup, { usageCollection })).toBe(undefined); + expect(coreSetup.coreUsageData.registerUsageCounter).toHaveBeenCalled(); + await expect( Promise.all( usageCollectors.map(async (usageCollector) => { diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index dadb4283e84a7..275dcc761125e 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -73,6 +73,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { usageCollection.createUsageCounter('uiCounters'); this.eventLoopUsageCounter = usageCollection.createUsageCounter('eventLoop'); + coreSetup.coreUsageData.registerUsageCounter(usageCollection.createUsageCounter('core')); this.registerUsageCollectors( usageCollection, coreSetup, diff --git a/src/plugins/vis_type_metric/tsconfig.json b/src/plugins/vis_type_metric/tsconfig.json deleted file mode 100644 index e430ec2460796..0000000000000 --- a/src/plugins/vis_type_metric/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["public/**/*", "server/**/*", "*.ts"], - "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../data/tsconfig.json" }, - { "path": "../visualizations/tsconfig.json" }, - { "path": "../charts/tsconfig.json" }, - { "path": "../expressions/tsconfig.json" }, - { "path": "../kibana_utils/tsconfig.json" }, - { "path": "../vis_default_editor/tsconfig.json" }, - { "path": "../field_formats/tsconfig.json" } - ] -} diff --git a/src/plugins/vis_type_tagcloud/tsconfig.json b/src/plugins/vis_type_tagcloud/tsconfig.json deleted file mode 100644 index 043eed06c6bcb..0000000000000 --- a/src/plugins/vis_type_tagcloud/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "public/**/*", - "server/**/*", - "*.ts" - ], - "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../data/tsconfig.json" }, - { "path": "../expressions/tsconfig.json" }, - { "path": "../visualizations/tsconfig.json" }, - { "path": "../charts/tsconfig.json" }, - { "path": "../kibana_react/tsconfig.json" }, - { "path": "../vis_default_editor/tsconfig.json" }, - ] -} diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json deleted file mode 100644 index 62bdd0262b4a5..0000000000000 --- a/src/plugins/vis_type_vega/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true, - "strictNullChecks": false - }, - "include": [ - "server/**/*", - "public/**/*", - "*.ts", - // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 - "public/test_utils/vega_map_test.json" - ], - "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../data/tsconfig.json" }, - { "path": "../visualizations/tsconfig.json" }, - { "path": "../maps_ems/tsconfig.json" }, - { "path": "../expressions/tsconfig.json" }, - { "path": "../inspector/tsconfig.json" }, - { "path": "../home/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" }, - { "path": "../kibana_utils/tsconfig.json" }, - { "path": "../kibana_react/tsconfig.json" }, - { "path": "../vis_default_editor/tsconfig.json" }, - { "path": "../es_ui_shared/tsconfig.json" }, - ] -} diff --git a/src/plugins/vis_type_metric/config.ts b/src/plugins/vis_types/metric/config.ts similarity index 100% rename from src/plugins/vis_type_metric/config.ts rename to src/plugins/vis_types/metric/config.ts diff --git a/src/plugins/vis_type_vega/jest.config.js b/src/plugins/vis_types/metric/jest.config.js similarity index 83% rename from src/plugins/vis_type_vega/jest.config.js rename to src/plugins/vis_types/metric/jest.config.js index c3e2ea5203364..a84929a3805b8 100644 --- a/src/plugins/vis_type_vega/jest.config.js +++ b/src/plugins/vis_types/metric/jest.config.js @@ -8,6 +8,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/vis_type_vega'], + rootDir: '../../../..', + roots: ['/src/plugins/vis_types/metric'], }; diff --git a/src/plugins/vis_type_metric/kibana.json b/src/plugins/vis_types/metric/kibana.json similarity index 100% rename from src/plugins/vis_type_metric/kibana.json rename to src/plugins/vis_types/metric/kibana.json diff --git a/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap b/src/plugins/vis_types/metric/public/__snapshots__/metric_vis_fn.test.ts.snap similarity index 100% rename from src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap rename to src/plugins/vis_types/metric/public/__snapshots__/metric_vis_fn.test.ts.snap diff --git a/src/plugins/vis_type_metric/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap similarity index 100% rename from src/plugins/vis_type_metric/public/__snapshots__/to_ast.test.ts.snap rename to src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap diff --git a/src/plugins/vis_type_metric/public/components/__snapshots__/metric_vis_component.test.tsx.snap b/src/plugins/vis_types/metric/public/components/__snapshots__/metric_vis_component.test.tsx.snap similarity index 100% rename from src/plugins/vis_type_metric/public/components/__snapshots__/metric_vis_component.test.tsx.snap rename to src/plugins/vis_types/metric/public/components/__snapshots__/metric_vis_component.test.tsx.snap diff --git a/src/plugins/vis_type_metric/public/components/metric_vis.scss b/src/plugins/vis_types/metric/public/components/metric_vis.scss similarity index 100% rename from src/plugins/vis_type_metric/public/components/metric_vis.scss rename to src/plugins/vis_types/metric/public/components/metric_vis.scss diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_component.test.tsx b/src/plugins/vis_types/metric/public/components/metric_vis_component.test.tsx similarity index 100% rename from src/plugins/vis_type_metric/public/components/metric_vis_component.test.tsx rename to src/plugins/vis_types/metric/public/components/metric_vis_component.test.tsx diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx b/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx similarity index 95% rename from src/plugins/vis_type_metric/public/components/metric_vis_component.tsx rename to src/plugins/vis_types/metric/public/components/metric_vis_component.tsx index 87ca902f6c090..c3735bdc0d79a 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx +++ b/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx @@ -11,13 +11,13 @@ import React, { Component } from 'react'; import { isColorDark } from '@elastic/eui'; import { MetricVisValue } from './metric_vis_value'; import { Input } from '../metric_vis_fn'; -import type { FieldFormatsContentType, IFieldFormat } from '../../../field_formats/common'; -import { Datatable } from '../../../expressions/public'; -import { getHeatmapColors } from '../../../charts/public'; +import type { FieldFormatsContentType, IFieldFormat } from '../../../../field_formats/common'; +import { Datatable } from '../../../../expressions/public'; +import { getHeatmapColors } from '../../../../charts/public'; import { VisParams, MetricVisMetric } from '../types'; import { getFormatService } from '../services'; -import { SchemaConfig } from '../../../visualizations/public'; -import { Range } from '../../../expressions/public'; +import { SchemaConfig } from '../../../../visualizations/public'; +import { Range } from '../../../../expressions/public'; import './metric_vis.scss'; diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/plugins/vis_types/metric/public/components/metric_vis_options.tsx similarity index 98% rename from src/plugins/vis_type_metric/public/components/metric_vis_options.tsx rename to src/plugins/vis_types/metric/public/components/metric_vis_options.tsx index 5c6c4bf95b4f2..22152b331a907 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/plugins/vis_types/metric/public/components/metric_vis_options.tsx @@ -27,8 +27,8 @@ import { ColorSchemaOptions, RangeOption, PercentageModeOption, -} from '../../../vis_default_editor/public'; -import { ColorMode, colorSchemas } from '../../../charts/public'; +} from '../../../../vis_default_editor/public'; +import { ColorMode, colorSchemas } from '../../../../charts/public'; import { MetricVisParam, VisParams } from '../types'; const metricColorMode = [ diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_value.test.tsx b/src/plugins/vis_types/metric/public/components/metric_vis_value.test.tsx similarity index 100% rename from src/plugins/vis_type_metric/public/components/metric_vis_value.test.tsx rename to src/plugins/vis_types/metric/public/components/metric_vis_value.test.tsx diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx b/src/plugins/vis_types/metric/public/components/metric_vis_value.tsx similarity index 100% rename from src/plugins/vis_type_metric/public/components/metric_vis_value.tsx rename to src/plugins/vis_types/metric/public/components/metric_vis_value.tsx diff --git a/src/plugins/vis_type_metric/public/index.ts b/src/plugins/vis_types/metric/public/index.ts similarity index 100% rename from src/plugins/vis_type_metric/public/index.ts rename to src/plugins/vis_types/metric/public/index.ts diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts b/src/plugins/vis_types/metric/public/metric_vis_fn.test.ts similarity index 90% rename from src/plugins/vis_type_metric/public/metric_vis_fn.test.ts rename to src/plugins/vis_types/metric/public/metric_vis_fn.test.ts index 432b1f2fe02b7..3844c0f21ed05 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts +++ b/src/plugins/vis_types/metric/public/metric_vis_fn.test.ts @@ -7,8 +7,8 @@ */ import { createMetricVisFn } from './metric_vis_fn'; -import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; -import { Datatable } from '../../expressions/common/expression_types/specs'; +import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils'; +import { Datatable } from '../../../expressions/common/expression_types/specs'; describe('interpreter/functions#metric', () => { const fn = functionWrapper(createMetricVisFn()); diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.ts b/src/plugins/vis_types/metric/public/metric_vis_fn.ts similarity index 97% rename from src/plugins/vis_type_metric/public/metric_vis_fn.ts rename to src/plugins/vis_types/metric/public/metric_vis_fn.ts index ab62c3e941e28..9a144defed4e7 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_types/metric/public/metric_vis_fn.ts @@ -14,10 +14,10 @@ import { Range, Render, Style, -} from '../../expressions/public'; +} from '../../../expressions/public'; import { visType, DimensionsVisParam, VisParams } from './types'; -import { prepareLogTable, Dimension } from '../../visualizations/public'; -import { ColorSchemas, vislibColorMaps, ColorMode } from '../../charts/public'; +import { prepareLogTable, Dimension } from '../../../visualizations/public'; +import { ColorSchemas, vislibColorMaps, ColorMode } from '../../../charts/public'; export type Input = Datatable; diff --git a/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx b/src/plugins/vis_types/metric/public/metric_vis_renderer.tsx similarity index 88% rename from src/plugins/vis_type_metric/public/metric_vis_renderer.tsx rename to src/plugins/vis_types/metric/public/metric_vis_renderer.tsx index 0fc904a325a99..0bd2efbfe2efb 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx +++ b/src/plugins/vis_types/metric/public/metric_vis_renderer.tsx @@ -9,8 +9,8 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { VisualizationContainer } from '../../visualizations/public'; -import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; +import { VisualizationContainer } from '../../../visualizations/public'; +import { ExpressionRenderDefinition } from '../../../expressions/common/expression_renderers'; import { MetricVisRenderValue } from './metric_vis_fn'; // @ts-ignore const MetricVisComponent = lazy(() => import('./components/metric_vis_component')); diff --git a/src/plugins/vis_type_metric/public/metric_vis_type.ts b/src/plugins/vis_types/metric/public/metric_vis_type.ts similarity index 93% rename from src/plugins/vis_type_metric/public/metric_vis_type.ts rename to src/plugins/vis_types/metric/public/metric_vis_type.ts index 382ef925c5282..9fc3856ba0edf 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/plugins/vis_types/metric/public/metric_vis_type.ts @@ -8,9 +8,9 @@ import { i18n } from '@kbn/i18n'; import { MetricVisOptions } from './components/metric_vis_options'; -import { ColorSchemas, ColorMode } from '../../charts/public'; -import { VisTypeDefinition } from '../../visualizations/public'; -import { AggGroupNames } from '../../data/public'; +import { ColorSchemas, ColorMode } from '../../../charts/public'; +import { VisTypeDefinition } from '../../../visualizations/public'; +import { AggGroupNames } from '../../../data/public'; import { toExpressionAst } from './to_ast'; import { VisParams } from './types'; diff --git a/src/plugins/vis_type_metric/public/plugin.ts b/src/plugins/vis_types/metric/public/plugin.ts similarity index 86% rename from src/plugins/vis_type_metric/public/plugin.ts rename to src/plugins/vis_types/metric/public/plugin.ts index 051a10eb3c72f..205c02d8e9c3b 100644 --- a/src/plugins/vis_type_metric/public/plugin.ts +++ b/src/plugins/vis_types/metric/public/plugin.ts @@ -7,13 +7,13 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public'; +import { VisualizationsSetup } from '../../../visualizations/public'; import { createMetricVisFn } from './metric_vis_fn'; import { createMetricVisTypeDefinition } from './metric_vis_type'; -import { ChartsPluginSetup } from '../../charts/public'; -import { DataPublicPluginStart } from '../../data/public'; +import { ChartsPluginSetup } from '../../../charts/public'; +import { DataPublicPluginStart } from '../../../data/public'; import { setFormatService } from './services'; import { ConfigSchema } from '../config'; import { metricVisRenderer } from './metric_vis_renderer'; diff --git a/src/plugins/vis_type_metric/public/services.ts b/src/plugins/vis_types/metric/public/services.ts similarity index 79% rename from src/plugins/vis_type_metric/public/services.ts rename to src/plugins/vis_types/metric/public/services.ts index 96d6b0f7a1cd3..e705513675e71 100644 --- a/src/plugins/vis_type_metric/public/services.ts +++ b/src/plugins/vis_types/metric/public/services.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { createGetterSetter } from '../../kibana_utils/common'; -import { DataPublicPluginStart } from '../../data/public'; +import { createGetterSetter } from '../../../kibana_utils/common'; +import { DataPublicPluginStart } from '../../../data/public'; export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] diff --git a/src/plugins/vis_type_metric/public/to_ast.test.ts b/src/plugins/vis_types/metric/public/to_ast.test.ts similarity index 100% rename from src/plugins/vis_type_metric/public/to_ast.test.ts rename to src/plugins/vis_types/metric/public/to_ast.test.ts diff --git a/src/plugins/vis_type_metric/public/to_ast.ts b/src/plugins/vis_types/metric/public/to_ast.ts similarity index 97% rename from src/plugins/vis_type_metric/public/to_ast.ts rename to src/plugins/vis_types/metric/public/to_ast.ts index ec9c2b3b0157e..10c782c9a50fb 100644 --- a/src/plugins/vis_type_metric/public/to_ast.ts +++ b/src/plugins/vis_types/metric/public/to_ast.ts @@ -7,13 +7,13 @@ */ import { get } from 'lodash'; -import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '../../visualizations/public'; -import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; import { MetricVisExpressionFunctionDefinition } from './metric_vis_fn'; import { EsaggsExpressionFunctionDefinition, IndexPatternLoadExpressionFunctionDefinition, -} from '../../data/public'; +} from '../../../data/public'; import { VisParams } from './types'; const prepareDimension = (params: SchemaConfig) => { diff --git a/src/plugins/vis_type_metric/public/types.ts b/src/plugins/vis_types/metric/public/types.ts similarity index 84% rename from src/plugins/vis_type_metric/public/types.ts rename to src/plugins/vis_types/metric/public/types.ts index 45b8e17425891..1baaa25959f31 100644 --- a/src/plugins/vis_type_metric/public/types.ts +++ b/src/plugins/vis_types/metric/public/types.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { Range } from '../../expressions/public'; -import { SchemaConfig } from '../../visualizations/public'; -import { ColorMode, Labels, Style, ColorSchemas } from '../../charts/public'; +import { Range } from '../../../expressions/public'; +import { SchemaConfig } from '../../../visualizations/public'; +import { ColorMode, Labels, Style, ColorSchemas } from '../../../charts/public'; export const visType = 'metric'; diff --git a/src/plugins/vis_type_metric/server/index.ts b/src/plugins/vis_types/metric/server/index.ts similarity index 100% rename from src/plugins/vis_type_metric/server/index.ts rename to src/plugins/vis_types/metric/server/index.ts diff --git a/src/plugins/vis_types/metric/tsconfig.json b/src/plugins/vis_types/metric/tsconfig.json new file mode 100644 index 0000000000000..e8c878425ff70 --- /dev/null +++ b/src/plugins/vis_types/metric/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*", "*.ts"], + "references": [ + { "path": "../../../core/tsconfig.json" }, + { "path": "../../data/tsconfig.json" }, + { "path": "../../visualizations/tsconfig.json" }, + { "path": "../../charts/tsconfig.json" }, + { "path": "../../expressions/tsconfig.json" }, + { "path": "../../kibana_utils/tsconfig.json" }, + { "path": "../../vis_default_editor/tsconfig.json" }, + { "path": "../../field_formats/tsconfig.json" } + ] +} diff --git a/src/plugins/vis_type_tagcloud/config.ts b/src/plugins/vis_types/tagcloud/config.ts similarity index 100% rename from src/plugins/vis_type_tagcloud/config.ts rename to src/plugins/vis_types/tagcloud/config.ts diff --git a/src/plugins/vis_type_tagcloud/jest.config.js b/src/plugins/vis_types/tagcloud/jest.config.js similarity index 84% rename from src/plugins/vis_type_tagcloud/jest.config.js rename to src/plugins/vis_types/tagcloud/jest.config.js index 3d24c536792bb..20dfd8ad0d11c 100644 --- a/src/plugins/vis_type_tagcloud/jest.config.js +++ b/src/plugins/vis_types/tagcloud/jest.config.js @@ -8,7 +8,7 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/vis_type_tagcloud'], + rootDir: '../../../..', + roots: ['/src/plugins/vis_types/tagcloud'], testRunner: 'jasmine2', }; diff --git a/src/plugins/vis_type_tagcloud/kibana.json b/src/plugins/vis_types/tagcloud/kibana.json similarity index 100% rename from src/plugins/vis_type_tagcloud/kibana.json rename to src/plugins/vis_types/tagcloud/kibana.json diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap similarity index 100% rename from src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap rename to src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap diff --git a/src/plugins/vis_type_tagcloud/public/components/collections.ts b/src/plugins/vis_types/tagcloud/public/components/collections.ts similarity index 100% rename from src/plugins/vis_type_tagcloud/public/components/collections.ts rename to src/plugins/vis_types/tagcloud/public/components/collections.ts diff --git a/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx b/src/plugins/vis_types/tagcloud/public/components/get_tag_cloud_options.tsx similarity index 100% rename from src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx rename to src/plugins/vis_types/tagcloud/public/components/get_tag_cloud_options.tsx diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_types/tagcloud/public/components/tag_cloud_options.tsx similarity index 92% rename from src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx rename to src/plugins/vis_types/tagcloud/public/components/tag_cloud_options.tsx index 6682799a8038a..ff5f1e6edd6f8 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_types/tagcloud/public/components/tag_cloud_options.tsx @@ -9,10 +9,10 @@ import React, { useState, useEffect } from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { PaletteRegistry } from '../../../charts/public'; -import { VisEditorOptionsProps } from '../../../visualizations/public'; -import { SelectOption, SwitchOption, PalettePicker } from '../../../vis_default_editor/public'; -import { ValidatedDualRange } from '../../../kibana_react/public'; +import type { PaletteRegistry } from '../../../../charts/public'; +import { VisEditorOptionsProps } from '../../../../visualizations/public'; +import { SelectOption, SwitchOption, PalettePicker } from '../../../../vis_default_editor/public'; +import { ValidatedDualRange } from '../../../../kibana_react/public'; import { TagCloudVisParams, TagCloudTypeProps } from '../types'; import { collections } from './collections'; diff --git a/src/plugins/vis_type_tagcloud/public/index.ts b/src/plugins/vis_types/tagcloud/public/index.ts similarity index 100% rename from src/plugins/vis_type_tagcloud/public/index.ts rename to src/plugins/vis_types/tagcloud/public/index.ts diff --git a/src/plugins/vis_type_tagcloud/public/plugin.ts b/src/plugins/vis_types/tagcloud/public/plugin.ts similarity index 91% rename from src/plugins/vis_type_tagcloud/public/plugin.ts rename to src/plugins/vis_types/tagcloud/public/plugin.ts index 06e1c516d9e61..cc99480de7099 100644 --- a/src/plugins/vis_type_tagcloud/public/plugin.ts +++ b/src/plugins/vis_types/tagcloud/public/plugin.ts @@ -7,8 +7,8 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { VisualizationsSetup } from '../../visualizations/public'; -import { ChartsPluginSetup } from '../../charts/public'; +import { VisualizationsSetup } from '../../../visualizations/public'; +import { ChartsPluginSetup } from '../../../charts/public'; import { getTagCloudVisTypeDefinition } from './tag_cloud_type'; import { ConfigSchema } from '../config'; diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_types/tagcloud/public/tag_cloud_type.ts similarity index 94% rename from src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts rename to src/plugins/vis_types/tagcloud/public/tag_cloud_type.ts index b3ab5cd3d7af7..a193a7fecc1fd 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_types/tagcloud/public/tag_cloud_type.ts @@ -7,8 +7,8 @@ */ import { i18n } from '@kbn/i18n'; -import { AggGroupNames } from '../../data/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; +import { AggGroupNames } from '../../../data/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; import { getTagCloudOptions } from './components/get_tag_cloud_options'; import { toExpressionAst } from './to_ast'; diff --git a/src/plugins/vis_type_tagcloud/public/to_ast.test.ts b/src/plugins/vis_types/tagcloud/public/to_ast.test.ts similarity index 97% rename from src/plugins/vis_type_tagcloud/public/to_ast.test.ts rename to src/plugins/vis_types/tagcloud/public/to_ast.test.ts index 4da9c525a4f93..c70448ab113cb 100644 --- a/src/plugins/vis_type_tagcloud/public/to_ast.test.ts +++ b/src/plugins/vis_types/tagcloud/public/to_ast.test.ts @@ -30,7 +30,7 @@ const mockSchemas = { ], }; -jest.mock('../../visualizations/public', () => ({ +jest.mock('../../../visualizations/public', () => ({ getVisSchemas: () => mockSchemas, })); diff --git a/src/plugins/vis_type_tagcloud/public/to_ast.ts b/src/plugins/vis_types/tagcloud/public/to_ast.ts similarity index 95% rename from src/plugins/vis_type_tagcloud/public/to_ast.ts rename to src/plugins/vis_types/tagcloud/public/to_ast.ts index c8810aa0397ee..b5256c586d1da 100644 --- a/src/plugins/vis_type_tagcloud/public/to_ast.ts +++ b/src/plugins/vis_types/tagcloud/public/to_ast.ts @@ -9,9 +9,9 @@ import { EsaggsExpressionFunctionDefinition, IndexPatternLoadExpressionFunctionDefinition, -} from '../../data/public'; -import { buildExpression, buildExpressionFunction } from '../../expressions/public'; -import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '../../visualizations/public'; +} from '../../../data/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '../../../visualizations/public'; import { TagCloudVisParams } from './types'; const prepareDimension = (params: SchemaConfig) => { diff --git a/src/plugins/vis_type_tagcloud/public/types.ts b/src/plugins/vis_types/tagcloud/public/types.ts similarity index 85% rename from src/plugins/vis_type_tagcloud/public/types.ts rename to src/plugins/vis_types/tagcloud/public/types.ts index d855ae5ab65c6..28a7c6506eb31 100644 --- a/src/plugins/vis_type_tagcloud/public/types.ts +++ b/src/plugins/vis_types/tagcloud/public/types.ts @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { ChartsPluginSetup, PaletteOutput } from '../../charts/public'; -import type { SerializedFieldFormat } from '../../expressions/public'; +import type { ChartsPluginSetup, PaletteOutput } from '../../../charts/public'; +import type { SerializedFieldFormat } from '../../../expressions/public'; interface Dimension { accessor: number; diff --git a/src/plugins/vis_type_tagcloud/server/index.ts b/src/plugins/vis_types/tagcloud/server/index.ts similarity index 100% rename from src/plugins/vis_type_tagcloud/server/index.ts rename to src/plugins/vis_types/tagcloud/server/index.ts diff --git a/src/plugins/vis_types/tagcloud/tsconfig.json b/src/plugins/vis_types/tagcloud/tsconfig.json new file mode 100644 index 0000000000000..4087f9f04c92b --- /dev/null +++ b/src/plugins/vis_types/tagcloud/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../../core/tsconfig.json" }, + { "path": "../../data/tsconfig.json" }, + { "path": "../../expressions/tsconfig.json" }, + { "path": "../../visualizations/tsconfig.json" }, + { "path": "../../charts/tsconfig.json" }, + { "path": "../../kibana_react/tsconfig.json" }, + { "path": "../../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_type_vega/config.ts b/src/plugins/vis_types/vega/config.ts similarity index 100% rename from src/plugins/vis_type_vega/config.ts rename to src/plugins/vis_types/vega/config.ts diff --git a/src/plugins/vis_type_metric/jest.config.js b/src/plugins/vis_types/vega/jest.config.js similarity index 83% rename from src/plugins/vis_type_metric/jest.config.js rename to src/plugins/vis_types/vega/jest.config.js index 3029320d2e4d4..d7e1653e891a5 100644 --- a/src/plugins/vis_type_metric/jest.config.js +++ b/src/plugins/vis_types/vega/jest.config.js @@ -8,6 +8,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/vis_type_metric'], + rootDir: '../../../..', + roots: ['/src/plugins/vis_types/vega'], }; diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_types/vega/kibana.json similarity index 100% rename from src/plugins/vis_type_vega/kibana.json rename to src/plugins/vis_types/vega/kibana.json diff --git a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap b/src/plugins/vis_types/vega/public/__snapshots__/vega_visualization.test.js.snap similarity index 100% rename from src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap rename to src/plugins/vis_types/vega/public/__snapshots__/vega_visualization.test.js.snap diff --git a/src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx b/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx similarity index 96% rename from src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx rename to src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx index ca0cb0f0ff797..2de6eb490196c 100644 --- a/src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx +++ b/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx @@ -10,7 +10,7 @@ import { parse } from 'hjson'; import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../visualizations/public'; +import { Vis } from '../../../../visualizations/public'; function ExperimentalMapLayerInfo() { const title = ( diff --git a/src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx b/src/plugins/vis_types/vega/public/components/vega_actions_menu.tsx similarity index 100% rename from src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx rename to src/plugins/vis_types/vega/public/components/vega_actions_menu.tsx diff --git a/src/plugins/vis_type_vega/public/components/vega_editor.scss b/src/plugins/vis_types/vega/public/components/vega_editor.scss similarity index 100% rename from src/plugins/vis_type_vega/public/components/vega_editor.scss rename to src/plugins/vis_types/vega/public/components/vega_editor.scss diff --git a/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx b/src/plugins/vis_types/vega/public/components/vega_help_menu.tsx similarity index 100% rename from src/plugins/vis_type_vega/public/components/vega_help_menu.tsx rename to src/plugins/vis_types/vega/public/components/vega_help_menu.tsx diff --git a/src/plugins/vis_type_vega/public/components/vega_vis.scss b/src/plugins/vis_types/vega/public/components/vega_vis.scss similarity index 100% rename from src/plugins/vis_type_vega/public/components/vega_vis.scss rename to src/plugins/vis_types/vega/public/components/vega_vis.scss diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_component.tsx b/src/plugins/vis_types/vega/public/components/vega_vis_component.tsx similarity index 100% rename from src/plugins/vis_type_vega/public/components/vega_vis_component.tsx rename to src/plugins/vis_types/vega/public/components/vega_vis_component.tsx diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx similarity index 97% rename from src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx rename to src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx index 9150b31343799..d2f586eac9885 100644 --- a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx @@ -13,7 +13,7 @@ import 'brace/mode/hjson'; import { i18n } from '@kbn/i18n'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { EuiCodeEditor } from '../../../es_ui_shared/public'; +import { EuiCodeEditor } from '../../../../es_ui_shared/public'; import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx b/src/plugins/vis_types/vega/public/components/vega_vis_editor_lazy.tsx similarity index 100% rename from src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx rename to src/plugins/vis_types/vega/public/components/vega_vis_editor_lazy.tsx diff --git a/src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts b/src/plugins/vis_types/vega/public/data_model/ems_file_parser.ts similarity index 97% rename from src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts rename to src/plugins/vis_types/vega/public/data_model/ems_file_parser.ts index c79ebe02c0d3f..3001da2008e40 100644 --- a/src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts +++ b/src/plugins/vis_types/vega/public/data_model/ems_file_parser.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; -import { IServiceSettings, FileLayer } from '../../../maps_ems/public'; +import { IServiceSettings, FileLayer } from '../../../../maps_ems/public'; import { Data, UrlObject, EmsQueryRequest } from './types'; /** diff --git a/src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js b/src/plugins/vis_types/vega/public/data_model/es_query_parser.test.js similarity index 100% rename from src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js rename to src/plugins/vis_types/vega/public/data_model/es_query_parser.test.js diff --git a/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts b/src/plugins/vis_types/vega/public/data_model/es_query_parser.ts similarity index 100% rename from src/plugins/vis_type_vega/public/data_model/es_query_parser.ts rename to src/plugins/vis_types/vega/public/data_model/es_query_parser.ts diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.test.ts b/src/plugins/vis_types/vega/public/data_model/search_api.test.ts similarity index 95% rename from src/plugins/vis_type_vega/public/data_model/search_api.test.ts rename to src/plugins/vis_types/vega/public/data_model/search_api.test.ts index d0739453e43ec..27dc1627ae229 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.test.ts +++ b/src/plugins/vis_types/vega/public/data_model/search_api.test.ts @@ -7,9 +7,9 @@ */ import { extendSearchParamsWithRuntimeFields } from './search_api'; -import { dataPluginMock } from '../../../data/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; -import { getSearchParamsFromRequest, DataPublicPluginStart } from '../../../data/public'; +import { getSearchParamsFromRequest, DataPublicPluginStart } from '../../../../data/public'; const mockComputedFields = ( dataStart: DataPublicPluginStart, diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_types/vega/public/data_model/search_api.ts similarity index 95% rename from src/plugins/vis_type_vega/public/data_model/search_api.ts rename to src/plugins/vis_types/vega/public/data_model/search_api.ts index efdbf96e54f05..e00cf647930a8 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_types/vega/public/data_model/search_api.ts @@ -14,10 +14,10 @@ import { SearchRequest, DataPublicPluginStart, IEsSearchResponse, -} from '../../../data/public'; -import { search as dataPluginSearch } from '../../../data/public'; +} from '../../../../data/public'; +import { search as dataPluginSearch } from '../../../../data/public'; import type { VegaInspectorAdapters } from '../vega_inspector'; -import type { RequestResponder } from '../../../inspector/public'; +import type { RequestResponder } from '../../../../inspector/public'; /** @internal **/ export const extendSearchParamsWithRuntimeFields = async ( diff --git a/src/plugins/vis_type_vega/public/data_model/time_cache.test.js b/src/plugins/vis_types/vega/public/data_model/time_cache.test.js similarity index 100% rename from src/plugins/vis_type_vega/public/data_model/time_cache.test.js rename to src/plugins/vis_types/vega/public/data_model/time_cache.test.js diff --git a/src/plugins/vis_type_vega/public/data_model/time_cache.ts b/src/plugins/vis_types/vega/public/data_model/time_cache.ts similarity index 95% rename from src/plugins/vis_type_vega/public/data_model/time_cache.ts rename to src/plugins/vis_types/vega/public/data_model/time_cache.ts index 13c01b5fe83bc..cc9619e143437 100644 --- a/src/plugins/vis_type_vega/public/data_model/time_cache.ts +++ b/src/plugins/vis_types/vega/public/data_model/time_cache.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { TimefilterContract } from '../../../data/public'; -import { TimeRange } from '../../../data/common'; +import { TimefilterContract } from '../../../../data/public'; +import { TimeRange } from '../../../../data/common'; import { CacheBounds } from './types'; /** diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_types/vega/public/data_model/types.ts similarity index 100% rename from src/plugins/vis_type_vega/public/data_model/types.ts rename to src/plugins/vis_types/vega/public/data_model/types.ts diff --git a/src/plugins/vis_type_vega/public/data_model/url_parser.ts b/src/plugins/vis_types/vega/public/data_model/url_parser.ts similarity index 100% rename from src/plugins/vis_type_vega/public/data_model/url_parser.ts rename to src/plugins/vis_types/vega/public/data_model/url_parser.ts diff --git a/src/plugins/vis_type_vega/public/data_model/utils.ts b/src/plugins/vis_types/vega/public/data_model/utils.ts similarity index 100% rename from src/plugins/vis_type_vega/public/data_model/utils.ts rename to src/plugins/vis_types/vega/public/data_model/utils.ts diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js similarity index 100% rename from src/plugins/vis_type_vega/public/data_model/vega_parser.test.js rename to src/plugins/vis_types/vega/public/data_model/vega_parser.test.js diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts similarity index 99% rename from src/plugins/vis_type_vega/public/data_model/vega_parser.ts rename to src/plugins/vis_types/vega/public/data_model/vega_parser.ts index bc7d72c042841..3ae95c0393b5a 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts @@ -22,7 +22,7 @@ import { EmsFileParser } from './ems_file_parser'; import { UrlParser } from './url_parser'; import { SearchAPI } from './search_api'; import { TimeCache } from './time_cache'; -import { IServiceSettings } from '../../../maps_ems/public'; +import { IServiceSettings } from '../../../../maps_ems/public'; import { Bool, Data, diff --git a/src/plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_types/vega/public/default.spec.hjson similarity index 100% rename from src/plugins/vis_type_vega/public/default.spec.hjson rename to src/plugins/vis_types/vega/public/default.spec.hjson diff --git a/src/plugins/vis_type_vega/public/default_spec.ts b/src/plugins/vis_types/vega/public/default_spec.ts similarity index 100% rename from src/plugins/vis_type_vega/public/default_spec.ts rename to src/plugins/vis_types/vega/public/default_spec.ts diff --git a/src/plugins/vis_type_vega/public/index.ts b/src/plugins/vis_types/vega/public/index.ts similarity index 100% rename from src/plugins/vis_type_vega/public/index.ts rename to src/plugins/vis_types/vega/public/index.ts diff --git a/src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts b/src/plugins/vis_types/vega/public/lib/extract_index_pattern.test.ts similarity index 97% rename from src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts rename to src/plugins/vis_types/vega/public/lib/extract_index_pattern.test.ts index 6feb8fce9c5a3..39aadc009b93c 100644 --- a/src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts +++ b/src/plugins/vis_types/vega/public/lib/extract_index_pattern.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { dataPluginMock } from '../../../data/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; import { extractIndexPatternsFromSpec } from './extract_index_pattern'; import { setData } from '../services'; diff --git a/src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts b/src/plugins/vis_types/vega/public/lib/extract_index_pattern.ts similarity index 94% rename from src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts rename to src/plugins/vis_types/vega/public/lib/extract_index_pattern.ts index 81f30ca9bbe2c..0d25db665ce7f 100644 --- a/src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts +++ b/src/plugins/vis_types/vega/public/lib/extract_index_pattern.ts @@ -10,7 +10,7 @@ import { flatten } from 'lodash'; import { getData } from '../services'; import type { Data, VegaSpec } from '../data_model/types'; -import type { IndexPattern } from '../../../data/public'; +import type { IndexPattern } from '../../../../data/public'; export const extractIndexPatternsFromSpec = async (spec: VegaSpec) => { const { indexPatterns } = getData(); diff --git a/src/plugins/vis_type_vega/public/lib/vega_state_restorer.test.ts b/src/plugins/vis_types/vega/public/lib/vega_state_restorer.test.ts similarity index 100% rename from src/plugins/vis_type_vega/public/lib/vega_state_restorer.test.ts rename to src/plugins/vis_types/vega/public/lib/vega_state_restorer.test.ts diff --git a/src/plugins/vis_type_vega/public/lib/vega_state_restorer.ts b/src/plugins/vis_types/vega/public/lib/vega_state_restorer.ts similarity index 100% rename from src/plugins/vis_type_vega/public/lib/vega_state_restorer.ts rename to src/plugins/vis_types/vega/public/lib/vega_state_restorer.ts diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_types/vega/public/plugin.ts similarity index 89% rename from src/plugins/vis_type_vega/public/plugin.ts rename to src/plugins/vis_types/vega/public/plugin.ts index f935362d21604..942c1673ad202 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_types/vega/public/plugin.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; -import { VisualizationsSetup } from '../../visualizations/public'; -import { Setup as InspectorSetup } from '../../inspector/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../data/public'; +import { VisualizationsSetup } from '../../../visualizations/public'; +import { Setup as InspectorSetup } from '../../../inspector/public'; import { setNotifications, @@ -24,7 +24,7 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; -import { IServiceSettings, MapsEmsPluginSetup } from '../../maps_ems/public'; +import { IServiceSettings, MapsEmsPluginSetup } from '../../../maps_ems/public'; import { ConfigSchema } from '../config'; import { getVegaInspectorView } from './vega_inspector'; diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_types/vega/public/services.ts similarity index 91% rename from src/plugins/vis_type_vega/public/services.ts rename to src/plugins/vis_types/vega/public/services.ts index f67fe4794e783..1cdb24ff7b02c 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_types/vega/public/services.ts @@ -8,8 +8,8 @@ import { CoreStart, NotificationsStart, IUiSettingsClient, DocLinksStart } from 'src/core/public'; -import { DataPublicPluginStart } from '../../data/public'; -import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { createGetterSetter } from '../../../kibana_utils/public'; import { MapServiceSettings } from './vega_view/vega_map_view/map_service_settings'; export const [getData, setData] = createGetterSetter('Data'); diff --git a/src/plugins/vis_type_vega/public/test_utils/default.spec.json b/src/plugins/vis_types/vega/public/test_utils/default.spec.json similarity index 100% rename from src/plugins/vis_type_vega/public/test_utils/default.spec.json rename to src/plugins/vis_types/vega/public/test_utils/default.spec.json diff --git a/src/plugins/vis_type_vega/public/test_utils/vega_graph.json b/src/plugins/vis_types/vega/public/test_utils/vega_graph.json similarity index 100% rename from src/plugins/vis_type_vega/public/test_utils/vega_graph.json rename to src/plugins/vis_types/vega/public/test_utils/vega_graph.json diff --git a/src/plugins/vis_type_vega/public/test_utils/vega_map_test.json b/src/plugins/vis_types/vega/public/test_utils/vega_map_test.json similarity index 100% rename from src/plugins/vis_type_vega/public/test_utils/vega_map_test.json rename to src/plugins/vis_types/vega/public/test_utils/vega_map_test.json diff --git a/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json b/src/plugins/vis_types/vega/public/test_utils/vegalite_graph.json similarity index 100% rename from src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json rename to src/plugins/vis_types/vega/public/test_utils/vegalite_graph.json diff --git a/src/plugins/vis_type_vega/public/to_ast.ts b/src/plugins/vis_types/vega/public/to_ast.ts similarity index 90% rename from src/plugins/vis_type_vega/public/to_ast.ts rename to src/plugins/vis_types/vega/public/to_ast.ts index 8f0bd952bc54e..f9ff7effafe49 100644 --- a/src/plugins/vis_type_vega/public/to_ast.ts +++ b/src/plugins/vis_types/vega/public/to_ast.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { buildExpression, buildExpressionFunction } from '../../expressions/public'; -import { Vis } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import { Vis } from '../../../visualizations/public'; import { VegaExpressionFunctionDefinition, VisParams } from './vega_fn'; export const toExpressionAst = (vis: Vis) => { diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_types/vega/public/vega_fn.ts similarity index 89% rename from src/plugins/vis_type_vega/public/vega_fn.ts rename to src/plugins/vis_types/vega/public/vega_fn.ts index 775bd2623028b..cbdc131ab7e2a 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_types/vega/public/vega_fn.ts @@ -8,12 +8,16 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExecutionContextSearch } from '../../data/public'; -import { ExecutionContext, ExpressionFunctionDefinition, Render } from '../../expressions/public'; +import { ExecutionContextSearch } from '../../../data/public'; +import { + ExecutionContext, + ExpressionFunctionDefinition, + Render, +} from '../../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; import { VegaInspectorAdapters } from './vega_inspector/index'; -import { KibanaContext, TimeRange, Query } from '../../data/public'; +import { KibanaContext, TimeRange, Query } from '../../../data/public'; import { VegaParser } from './data_model/vega_parser'; type Input = KibanaContext | { type: 'null' }; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx b/src/plugins/vis_types/vega/public/vega_inspector/components/data_viewer.tsx similarity index 100% rename from src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx rename to src/plugins/vis_types/vega/public/vega_inspector/components/data_viewer.tsx diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts b/src/plugins/vis_types/vega/public/vega_inspector/components/index.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_inspector/components/index.ts rename to src/plugins/vis_types/vega/public/vega_inspector/components/index.ts diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx b/src/plugins/vis_types/vega/public/vega_inspector/components/inspector_data_grid.tsx similarity index 100% rename from src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx rename to src/plugins/vis_types/vega/public/vega_inspector/components/inspector_data_grid.tsx diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx b/src/plugins/vis_types/vega/public/vega_inspector/components/signal_viewer.tsx similarity index 100% rename from src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx rename to src/plugins/vis_types/vega/public/vega_inspector/components/signal_viewer.tsx diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx b/src/plugins/vis_types/vega/public/vega_inspector/components/spec_viewer.tsx similarity index 97% rename from src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx rename to src/plugins/vis_types/vega/public/vega_inspector/components/spec_viewer.tsx index 135c23b21d7d4..9c13857076d8b 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx +++ b/src/plugins/vis_types/vega/public/vega_inspector/components/spec_viewer.tsx @@ -19,7 +19,7 @@ import { CommonProps, } from '@elastic/eui'; import { VegaAdapter } from '../vega_adapter'; -import { CodeEditor } from '../../../../kibana_react/public'; +import { CodeEditor } from '../../../../../kibana_react/public'; interface SpecViewerProps extends CommonProps { vegaAdapter: VegaAdapter; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/index.ts b/src/plugins/vis_types/vega/public/vega_inspector/index.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_inspector/index.ts rename to src/plugins/vis_types/vega/public/vega_inspector/index.ts diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts b/src/plugins/vis_types/vega/public/vega_inspector/vega_adapter.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts rename to src/plugins/vis_types/vega/public/vega_inspector/vega_adapter.ts diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss b/src/plugins/vis_types/vega/public/vega_inspector/vega_data_inspector.scss similarity index 100% rename from src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss rename to src/plugins/vis_types/vega/public/vega_inspector/vega_data_inspector.scss diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx b/src/plugins/vis_types/vega/public/vega_inspector/vega_data_inspector.tsx similarity index 96% rename from src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx rename to src/plugins/vis_types/vega/public/vega_inspector/vega_data_inspector.tsx index 497c8a9ed1af3..75618df307150 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx +++ b/src/plugins/vis_types/vega/public/vega_inspector/vega_data_inspector.tsx @@ -14,7 +14,7 @@ import { EuiTabbedContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { VegaInspectorAdapters } from './vega_inspector'; import { DataViewer, SignalViewer, SpecViewer } from './components'; -import { InspectorViewProps } from '../../../inspector/public'; +import { InspectorViewProps } from '../../../../inspector/public'; export type VegaDataInspectorProps = InspectorViewProps; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx b/src/plugins/vis_types/vega/public/vega_inspector/vega_inspector.tsx similarity index 94% rename from src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx rename to src/plugins/vis_types/vega/public/vega_inspector/vega_inspector.tsx index e02ae780acab1..1ca95b84f53ae 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx +++ b/src/plugins/vis_types/vega/public/vega_inspector/vega_inspector.tsx @@ -11,8 +11,8 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/public'; -import { KibanaContextProvider } from '../../../kibana_react/public'; -import { Adapters, RequestAdapter, InspectorViewDescription } from '../../../inspector/public'; +import { KibanaContextProvider } from '../../../../kibana_react/public'; +import { Adapters, RequestAdapter, InspectorViewDescription } from '../../../../inspector/public'; import { VegaAdapter } from './vega_adapter'; import type { VegaDataInspectorProps } from './vega_data_inspector'; diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_types/vega/public/vega_request_handler.ts similarity index 96% rename from src/plugins/vis_type_vega/public/vega_request_handler.ts rename to src/plugins/vis_types/vega/public/vega_request_handler.ts index 4f07785f43c4f..2ae7169c2f732 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_types/vega/public/vega_request_handler.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import type { KibanaExecutionContext } from 'src/core/public'; -import { Filter, esQuery, TimeRange, Query } from '../../data/public'; +import { Filter, esQuery, TimeRange, Query } from '../../../data/public'; import { SearchAPI } from './data_model/search_api'; import { TimeCache } from './data_model/time_cache'; diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_types/vega/public/vega_type.ts similarity index 95% rename from src/plugins/vis_type_vega/public/vega_type.ts rename to src/plugins/vis_types/vega/public/vega_type.ts index 902f79d03e680..74899f5cfb3a4 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_types/vega/public/vega_type.ts @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { parse } from 'hjson'; -import { DefaultEditorSize } from '../../vis_default_editor/public'; -import { VIS_EVENT_TO_TRIGGER, VisGroups, VisTypeDefinition } from '../../visualizations/public'; +import { DefaultEditorSize } from '../../../vis_default_editor/public'; +import { VIS_EVENT_TO_TRIGGER, VisGroups, VisTypeDefinition } from '../../../visualizations/public'; import { getDefaultSpec } from './default_spec'; import { extractIndexPatternsFromSpec } from './lib/extract_index_pattern'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.d.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_base_view.d.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js similarity index 99% rename from src/plugins/vis_type_vega/public/vega_view/vega_base_view.js rename to src/plugins/vis_types/vega/public/vega_view/vega_base_view.js index 0cf3f16c3d20c..a41197293bbdc 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js @@ -16,7 +16,7 @@ import { Utils } from '../data_model/utils'; import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; -import { esFilters } from '../../../data/public'; +import { esFilters } from '../../../../data/public'; import { getEnableExternalUrls, getData } from '../services'; import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/constants.ts similarity index 94% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/constants.ts index 04957fda5b8ff..a2a3cd464276f 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/constants.ts @@ -7,7 +7,7 @@ */ import type { Style } from '@kbn/mapbox-gl'; -import { TMS_IN_YML_ID } from '../../../../maps_ems/public'; +import { TMS_IN_YML_ID } from '../../../../../maps_ems/public'; export const vegaLayerId = 'vega'; export const userConfiguredLayerId = TMS_IN_YML_ID; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/index.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/index.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/index.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/index.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/tms_raster_layer.test.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.test.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/tms_raster_layer.test.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/tms_raster_layer.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/tms_raster_layer.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/types.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/types.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/vega_layer.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/layers/vega_layer.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.test.ts similarity index 98% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.test.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.test.ts index c459220d4aa86..95fee2ea3820d 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.test.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.test.ts @@ -10,7 +10,7 @@ import { get } from 'lodash'; import { uiSettingsServiceMock } from 'src/core/public/mocks'; import { MapServiceSettings, getAttributionsForTmsService } from './map_service_settings'; -import type { MapsEmsConfig } from '../../../../maps_ems/public'; +import type { MapsEmsConfig } from '../../../../../maps_ems/public'; import { EMSClient, TMSService } from '@elastic/ems-client'; import { setUISettings } from '../../services'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts similarity index 97% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts index 8874db7737a4e..3399d0628ad65 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import type { EMSClient, TMSService } from '@elastic/ems-client'; import { getUISettings } from '../../services'; import { userConfiguredLayerId } from './constants'; -import type { MapsEmsConfig } from '../../../../maps_ems/public'; +import type { MapsEmsConfig } from '../../../../../maps_ems/public'; type EmsClientConfig = ConstructorParameters[0]; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/index.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/index.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/index.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/index.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/validation_helper.test.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.test.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/validation_helper.test.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/validation_helper.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/validation_helper.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/vsi_helper.test.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.test.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/vsi_helper.test.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/vsi_helper.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/utils/vsi_helper.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/vega_map_view.scss similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/vega_map_view.scss diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts similarity index 97% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts index 17a098649ebbf..e4bf4977094fd 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts @@ -15,9 +15,9 @@ import { VegaParser } from '../../data_model/vega_parser'; import { TimeCache } from '../../data_model/time_cache'; import { SearchAPI } from '../../data_model/search_api'; import vegaMap from '../../test_utils/vega_map_test.json'; -import { coreMock } from '../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../data/public/mocks'; -import type { IServiceSettings, MapsEmsConfig } from '../../../../maps_ems/public'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../../data/public/mocks'; +import type { IServiceSettings, MapsEmsConfig } from '../../../../../maps_ems/public'; import { MapServiceSettings } from './map_service_settings'; import { userConfiguredLayerId } from './constants'; import { diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_types/vega/public/vega_view/vega_tooltip.js similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js rename to src/plugins/vis_types/vega/public/vega_view/vega_tooltip.js diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.d.ts b/src/plugins/vis_types/vega/public/vega_view/vega_view.d.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_view.d.ts rename to src/plugins/vis_types/vega/public/vega_view/vega_view.d.ts diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_types/vega/public/vega_view/vega_view.js similarity index 100% rename from src/plugins/vis_type_vega/public/vega_view/vega_view.js rename to src/plugins/vis_types/vega/public/vega_view/vega_view.js diff --git a/src/plugins/vis_type_vega/public/vega_vis_renderer.tsx b/src/plugins/vis_types/vega/public/vega_vis_renderer.tsx similarity index 94% rename from src/plugins/vis_type_vega/public/vega_vis_renderer.tsx rename to src/plugins/vis_types/vega/public/vega_vis_renderer.tsx index 0d9ba493281fa..77af6dfdcf042 100644 --- a/src/plugins/vis_type_vega/public/vega_vis_renderer.tsx +++ b/src/plugins/vis_types/vega/public/vega_vis_renderer.tsx @@ -10,7 +10,7 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ExpressionRenderDefinition } from 'src/plugins/expressions'; -import { VisualizationContainer } from '../../visualizations/public'; +import { VisualizationContainer } from '../../../visualizations/public'; import { VegaVisualizationDependencies } from './plugin'; import { RenderValue } from './vega_fn'; const VegaVisComponent = lazy(() => import('./components/vega_vis_component')); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_types/vega/public/vega_visualization.test.js similarity index 97% rename from src/plugins/vis_type_vega/public/vega_visualization.test.js rename to src/plugins/vis_types/vega/public/vega_visualization.test.js index ba1121b8894e0..8c47b2fdfd7c0 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_types/vega/public/vega_visualization.test.js @@ -19,8 +19,8 @@ import { VegaParser } from './data_model/vega_parser'; import { SearchAPI } from './data_model/search_api'; import { setInjectedVars, setData, setNotifications } from './services'; -import { coreMock } from '../../../core/public/mocks'; -import { dataPluginMock } from '../../data/public/mocks'; +import { coreMock } from '../../../../core/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; jest.mock('./default_spec', () => ({ getDefaultSpec: () => jest.requireActual('./test_utils/default.spec.json'), diff --git a/src/plugins/vis_type_vega/public/vega_visualization.ts b/src/plugins/vis_types/vega/public/vega_visualization.ts similarity index 100% rename from src/plugins/vis_type_vega/public/vega_visualization.ts rename to src/plugins/vis_types/vega/public/vega_visualization.ts diff --git a/src/plugins/vis_type_vega/server/index.ts b/src/plugins/vis_types/vega/server/index.ts similarity index 100% rename from src/plugins/vis_type_vega/server/index.ts rename to src/plugins/vis_types/vega/server/index.ts diff --git a/src/plugins/vis_type_vega/server/plugin.ts b/src/plugins/vis_types/vega/server/plugin.ts similarity index 97% rename from src/plugins/vis_type_vega/server/plugin.ts rename to src/plugins/vis_types/vega/server/plugin.ts index b884dcd1a1d15..51a783456d6b2 100644 --- a/src/plugins/vis_type_vega/server/plugin.ts +++ b/src/plugins/vis_types/vega/server/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/server'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/server'; import { registerVegaUsageCollector } from './usage_collector'; import { ConfigObservable, diff --git a/src/plugins/vis_type_vega/server/types.ts b/src/plugins/vis_types/vega/server/types.ts similarity index 87% rename from src/plugins/vis_type_vega/server/types.ts rename to src/plugins/vis_types/vega/server/types.ts index affd93dedb8ca..626d22b59181a 100644 --- a/src/plugins/vis_type_vega/server/types.ts +++ b/src/plugins/vis_types/vega/server/types.ts @@ -8,8 +8,8 @@ import { Observable } from 'rxjs'; import { SharedGlobalConfig } from 'kibana/server'; -import { HomeServerPluginSetup } from '../../home/server'; -import { UsageCollectionSetup } from '../../usage_collection/server'; +import { HomeServerPluginSetup } from '../../../home/server'; +import { UsageCollectionSetup } from '../../../usage_collection/server'; export type ConfigObservable = Observable; diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts b/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.mock.ts similarity index 100% rename from src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts rename to src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.mock.ts diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.test.ts similarity index 94% rename from src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts rename to src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.test.ts index 82aba087dedc1..aa1b8e447bbca 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.test.ts @@ -7,9 +7,9 @@ */ import { getStats } from './get_usage_collector'; -import { createCollectorFetchContextMock } from '../../../usage_collection/server/mocks'; -import type { HomeServerPluginSetup } from '../../../home/server'; -import type { SavedObjectsClientContract } from '../../../../core/server'; +import { createCollectorFetchContextMock } from '../../../../usage_collection/server/mocks'; +import type { HomeServerPluginSetup } from '../../../../home/server'; +import type { SavedObjectsClientContract } from '../../../../../core/server'; const mockedSavedObjects = [ // vega-lite lib spec diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.ts similarity index 93% rename from src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts rename to src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.ts index ae99021745a0c..0e67af1c2e890 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.ts @@ -8,8 +8,11 @@ import { parse } from 'hjson'; -import type { SavedObjectsClientContract, SavedObjectsFindResult } from '../../../../core/server'; -import type { SavedVisState } from '../../../visualizations/common'; +import type { + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '../../../../../core/server'; +import type { SavedVisState } from '../../../../visualizations/common'; import type { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types'; type UsageCollectorDependencies = Pick; @@ -75,7 +78,7 @@ export const getStats = async ( const doTelemetry = ({ params }: SavedVisState) => { try { - const spec = parse(params.spec, { legacyRoot: false }); + const spec = parse(params.spec as string, { legacyRoot: false }); if (spec) { shouldPublishTelemetry = true; diff --git a/src/plugins/vis_type_vega/server/usage_collector/index.ts b/src/plugins/vis_types/vega/server/usage_collector/index.ts similarity index 100% rename from src/plugins/vis_type_vega/server/usage_collector/index.ts rename to src/plugins/vis_types/vega/server/usage_collector/index.ts diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_types/vega/server/usage_collector/register_vega_collector.test.ts similarity index 95% rename from src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts rename to src/plugins/vis_types/vega/server/usage_collector/register_vega_collector.test.ts index fc488540293ad..137dd2f17375c 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_types/vega/server/usage_collector/register_vega_collector.test.ts @@ -9,11 +9,11 @@ import { createUsageCollectionSetupMock, createCollectorFetchContextMock, -} from '../../../usage_collection/server/mocks'; +} from '../../../../usage_collection/server/mocks'; import { mockStats, mockGetStats } from './get_usage_collector.mock'; import { registerVegaUsageCollector } from './register_vega_collector'; -import type { HomeServerPluginSetup } from '../../../home/server'; +import type { HomeServerPluginSetup } from '../../../../home/server'; import type { ConfigObservable } from '../types'; describe('registerVegaUsageCollector', () => { diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts b/src/plugins/vis_types/vega/server/usage_collector/register_vega_collector.ts similarity index 93% rename from src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts rename to src/plugins/vis_types/vega/server/usage_collector/register_vega_collector.ts index ef65b58a8315b..27af5aa57e38b 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts +++ b/src/plugins/vis_types/vega/server/usage_collector/register_vega_collector.ts @@ -7,7 +7,7 @@ */ import { getStats, VegaUsage } from './get_usage_collector'; -import type { UsageCollectionSetup } from '../../../usage_collection/server'; +import type { UsageCollectionSetup } from '../../../../usage_collection/server'; import type { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types'; export function registerVegaUsageCollector( diff --git a/src/plugins/vis_types/vega/tsconfig.json b/src/plugins/vis_types/vega/tsconfig.json new file mode 100644 index 0000000000000..ed7690ac70d1a --- /dev/null +++ b/src/plugins/vis_types/vega/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "strictNullChecks": false + }, + "include": [ + "server/**/*", + "public/**/*", + "*.ts", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "public/test_utils/vega_map_test.json" + ], + "references": [ + { "path": "../../../core/tsconfig.json" }, + { "path": "../../data/tsconfig.json" }, + { "path": "../../visualizations/tsconfig.json" }, + { "path": "../../maps_ems/tsconfig.json" }, + { "path": "../../expressions/tsconfig.json" }, + { "path": "../../inspector/tsconfig.json" }, + { "path": "../../home/tsconfig.json" }, + { "path": "../../usage_collection/tsconfig.json" }, + { "path": "../../kibana_utils/tsconfig.json" }, + { "path": "../../kibana_react/tsconfig.json" }, + { "path": "../../vis_default_editor/tsconfig.json" }, + { "path": "../../es_ui_shared/tsconfig.json" }, + ] +} diff --git a/src/plugins/visualizations/common/types.ts b/src/plugins/visualizations/common/types.ts index 0bdae26f01f93..187e55566c9d9 100644 --- a/src/plugins/visualizations/common/types.ts +++ b/src/plugins/visualizations/common/types.ts @@ -7,18 +7,20 @@ */ import { SavedObjectAttributes } from 'kibana/server'; -import { AggConfigOptions } from 'src/plugins/data/common'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { AggConfigSerialized } from 'src/plugins/data/common'; export interface VisParams { [key: string]: any; } -export interface SavedVisState { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SavedVisState = { title: string; type: string; params: TVisParams; - aggs: AggConfigOptions[]; -} + aggs: AggConfigSerialized[]; +}; export interface VisualizationSavedObjectAttributes extends SavedObjectAttributes { description: string; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts index 867febd2544b0..9c832414e7f00 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts @@ -115,7 +115,7 @@ describe('injectReferences', () => { }); test('injects references into context', () => { - const context = { + const context = ({ id: '1', title: 'test', savedSearchRefName: 'search_0', @@ -133,7 +133,7 @@ describe('injectReferences', () => { ], }, } as unknown) as SavedVisState, - } as VisSavedObject; + } as unknown) as VisSavedObject; const references = [ { name: 'search_0', @@ -182,7 +182,7 @@ describe('injectReferences', () => { }); test(`fails when it can't find the index pattern reference in the array`, () => { - const context = { + const context = ({ id: '1', title: 'test', visState: ({ @@ -196,7 +196,7 @@ describe('injectReferences', () => { ], }, } as unknown) as SavedVisState, - } as VisSavedObject; + } as unknown) as VisSavedObject; expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( `"Could not find index pattern reference \\"control_0_index_pattern\\""` ); diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index c6eceb86b3450..d68599c0724f6 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -8,10 +8,10 @@ import { SavedObject } from '../../../plugins/saved_objects/public'; import { - AggConfigOptions, IAggConfigs, SearchSourceFields, TimefilterContract, + AggConfigSerialized, } from '../../../plugins/data/public'; import { ExpressionAstExpression } from '../../expressions/public'; @@ -24,7 +24,7 @@ export interface SavedVisState { title: string; type: string; params: VisParams; - aggs: AggConfigOptions[]; + aggs: AggConfigSerialized[]; } export interface ISavedVis { diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index ff4e8a3794e0d..dfab4ecfc3cd8 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -26,7 +26,7 @@ import { IAggConfigs, IndexPattern, ISearchSource, - AggConfigOptions, + AggConfigSerialized, SearchSourceFields, } from '../../../plugins/data/public'; import { BaseVisType } from './vis_types'; @@ -34,7 +34,7 @@ import { VisParams } from '../common/types'; export interface SerializedVisData { expression?: string; - aggs: AggConfigOptions[]; + aggs: AggConfigSerialized[]; searchSource: SearchSourceFields; savedSearchId?: string; } @@ -194,7 +194,7 @@ export class Vis { } } - private initializeDefaultsFromSchemas(configStates: AggConfigOptions[], schemas: any) { + private initializeDefaultsFromSchemas(configStates: AggConfigSerialized[], schemas: any) { // Set the defaults for any schema which has them. If the defaults // for some reason has more then the max only set the max number // of defaults (not sure why a someone define more... diff --git a/src/plugins/visualize/common/constants.ts b/src/plugins/visualize/common/constants.ts index 5fe8ed7e095a2..10a4498193e3d 100644 --- a/src/plugins/visualize/common/constants.ts +++ b/src/plugins/visualize/common/constants.ts @@ -8,3 +8,16 @@ export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; + +export const APP_NAME = 'visualize'; + +export const VisualizeConstants = { + VISUALIZE_BASE_PATH: '/app/visualize', + LANDING_PAGE_PATH: '/', + WIZARD_STEP_1_PAGE_PATH: '/new', + WIZARD_STEP_2_PAGE_PATH: '/new/configure', + CREATE_PATH: '/create', + EDIT_PATH: '/edit', + EDIT_BY_VALUE_PATH: '/edit_by_value', + APP_ID: 'visualize', +}; diff --git a/src/plugins/visualize/common/locator.test.ts b/src/plugins/visualize/common/locator.test.ts new file mode 100644 index 0000000000000..c08c6a910327e --- /dev/null +++ b/src/plugins/visualize/common/locator.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { VisualizeLocatorDefinition } from './locator'; +import { FilterStateStore } from '../../data/common'; + +describe('visualize locator', () => { + let definition: VisualizeLocatorDefinition; + + beforeEach(() => { + definition = new VisualizeLocatorDefinition(); + }); + + it('returns a location for "create" path', async () => { + const location = await definition.getLocation({}); + + expect(location.app).toMatchInlineSnapshot(`"visualize"`); + expect(location.path).toMatchInlineSnapshot(`"#/create?_g=()&_a=()"`); + expect(location.state).toMatchInlineSnapshot(`Object {}`); + }); + + it('returns a location for "edit" path', async () => { + const location = await definition.getLocation({ + visId: 'test', + vis: { + title: 'test', + type: 'test', + aggs: [], + params: {}, + }, + }); + + expect(location.app).toMatchInlineSnapshot(`"visualize"`); + expect(location.path).toMatchInlineSnapshot( + `"#/edit/test?_g=()&_a=(vis:(aggs:!(),params:(),title:test,type:test))&type=test"` + ); + expect(location.state).toMatchInlineSnapshot(`Object {}`); + }); + + it('creates a location with query, filters (global and app), refresh interval and time range', async () => { + const location = await definition.getLocation({ + visId: '123', + vis: { + title: 'test', + type: 'test', + aggs: [], + params: {}, + }, + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + }, + ], + query: { query: 'bye', language: 'kuery' }, + }); + + expect(location.app).toMatchInlineSnapshot(`"visualize"`); + + expect(location.path.match(/filters:/g)?.length).toBe(2); + expect(location.path.match(/refreshInterval:/g)?.length).toBe(1); + expect(location.path.match(/time:/g)?.length).toBe(1); + expect(location.path).toMatchInlineSnapshot( + `"#/edit/123?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye),vis:(aggs:!(),params:(),title:test,type:test))&type=test"` + ); + + expect(location.state).toMatchInlineSnapshot(`Object {}`); + }); + + it('creates a location with all values provided', async () => { + const indexPattern = 'indexPatternTest'; + const savedSearchId = 'savedSearchIdTest'; + const location = await definition.getLocation({ + visId: '123', + vis: { + title: 'test', + type: 'test', + aggs: [], + params: {}, + }, + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + }, + ], + query: { query: 'bye', language: 'kuery' }, + linked: true, + uiState: { + fakeUIState: 'fakeUIState', + this: 'value contains a spaces that should be encoded', + }, + indexPattern, + savedSearchId, + }); + + expect(location.app).toMatchInlineSnapshot(`"visualize"`); + expect(location.path).toContain(indexPattern); + expect(location.path).toContain(savedSearchId); + expect(location.path).toMatchInlineSnapshot( + `"#/edit/123?_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),linked:!t,query:(language:kuery,query:bye),uiState:(fakeUIState:fakeUIState,this:'value%20contains%20a%20spaces%20that%20should%20be%20encoded'),vis:(aggs:!(),params:(),title:test,type:test))&indexPattern=indexPatternTest&savedSearchId=savedSearchIdTest&type=test"` + ); + expect(location.state).toMatchInlineSnapshot(`Object {}`); + }); +}); diff --git a/src/plugins/visualize/common/locator.ts b/src/plugins/visualize/common/locator.ts new file mode 100644 index 0000000000000..23fde918780f2 --- /dev/null +++ b/src/plugins/visualize/common/locator.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableRecord, Serializable } from '@kbn/utility-types'; +import { omitBy } from 'lodash'; +import type { ParsedQuery } from 'query-string'; +import { stringify } from 'query-string'; +import rison from 'rison-node'; +import type { Filter, Query, RefreshInterval, TimeRange } from 'src/plugins/data/common'; +import type { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; +import { isFilterPinned } from '../../data/common'; +import { url } from '../../kibana_utils/common'; +import { GLOBAL_STATE_STORAGE_KEY, STATE_STORAGE_KEY, VisualizeConstants } from './constants'; +import { PureVisState } from './types'; + +const removeEmptyKeys = (o: Record): Record => + omitBy(o, (v) => v == null); + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type VisualizeLocatorParams = { + /** + * The ID of the saved visualization to load. + */ + visId?: string; + + /** + * Global- and app-level filters to apply to data loaded by visualize. + */ + filters?: Filter[]; + + /** + * Time range to apply to data loaded by visualize. + */ + timeRange?: TimeRange; + + /** + * How frequently to poll for data. + */ + refreshInterval?: RefreshInterval; + + /** + * The query to use in to load data in visualize. + */ + query?: Query; + + /** + * UI state to be passed on to the current visualization. This value is opaque from the perspective of visualize. + */ + uiState?: SerializableRecord; + + /** + * Serialized visualization. + * + * @note This is required to navigate to "create" page (i.e., when no `visId` has been provided). + */ + vis?: PureVisState; + + /** + * Whether this visualization is linked a saved search. + */ + linked?: boolean; + + /** + * The saved search used as the source of the visualization. + */ + savedSearchId?: string; + + /** + * The saved search used as the source of the visualization. + */ + indexPattern?: string; +}; + +export type VisualizeAppLocator = LocatorPublic; + +export const VISUALIZE_APP_LOCATOR = 'VISUALIZE_APP_LOCATOR'; + +export class VisualizeLocatorDefinition implements LocatorDefinition { + id = VISUALIZE_APP_LOCATOR; + + public async getLocation({ + visId, + timeRange, + filters, + refreshInterval, + linked, + uiState, + query, + vis, + savedSearchId, + indexPattern, + }: VisualizeLocatorParams) { + let path = visId + ? `#${VisualizeConstants.EDIT_PATH}/${visId}` + : `#${VisualizeConstants.CREATE_PATH}`; + + const urlState: ParsedQuery = { + [GLOBAL_STATE_STORAGE_KEY]: rison.encode( + removeEmptyKeys({ + time: timeRange, + filters: filters?.filter((f) => isFilterPinned(f)), + refreshInterval, + }) + ), + [STATE_STORAGE_KEY]: rison.encode( + removeEmptyKeys({ + linked, + filters: filters?.filter((f) => !isFilterPinned(f)), + uiState, + query, + vis, + }) + ), + }; + + path += `?${stringify(url.encodeQuery(urlState), { encode: false, sort: false })}`; + + const otherParams = stringify({ type: vis?.type, savedSearchId, indexPattern }); + + if (otherParams) path += `&${otherParams}`; + + return { + app: VisualizeConstants.APP_ID, + path, + state: {}, + }; + } +} diff --git a/src/plugins/visualize/common/types.ts b/src/plugins/visualize/common/types.ts new file mode 100644 index 0000000000000..189c44ba15cc8 --- /dev/null +++ b/src/plugins/visualize/common/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { SavedVisState } from 'src/plugins/visualizations/common/types'; + +export type PureVisState = SavedVisState; diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index f850aedc33366..7e9f69163f5a6 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -9,6 +9,8 @@ import type { EventEmitter } from 'events'; import type { History } from 'history'; +import type { SerializableRecord } from '@kbn/utility-types'; + import type { CoreStart, PluginInitializerContext, @@ -19,7 +21,6 @@ import type { } from 'kibana/public'; import type { - SavedVisState, VisualizationsStart, Vis, VisualizeEmbeddableContract, @@ -45,11 +46,11 @@ import type { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import type { UsageCollectionStart } from '../../../usage_collection/public'; -export type PureVisState = SavedVisState; +import { PureVisState } from '../../common/types'; export interface VisualizeAppState { filters: Filter[]; - uiState: Record; + uiState: SerializableRecord; vis: PureVisState; query: Query; savedQuery?: string; @@ -103,6 +104,7 @@ export interface VisualizeServices extends CoreStart { savedObjectsTagging?: SavedObjectsTaggingApi; presentationUtil: PresentationUtilPluginStart; usageCollection?: UsageCollectionStart; + getKibanaVersion: () => string; } export interface SavedVisInstance { @@ -146,3 +148,5 @@ export interface EditorRenderProps { */ linked: boolean; } + +export { PureVisState }; diff --git a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts index e288996fa6f3d..10c573090da34 100644 --- a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts +++ b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts @@ -63,7 +63,6 @@ const pureTransitions = { function createVisualizeByValueAppState(stateDefaults: VisualizeAppState) { const initialState = migrateAppState({ ...stateDefaults, - ...stateDefaults, }); const stateContainer = createStateContainer( initialState, diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index ed361bbdb104d..a4421d9535c71 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -7,8 +7,10 @@ */ import React from 'react'; +import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; +import { parse } from 'query-string'; import { Capabilities } from 'src/core/public'; import { TopNavMenuData } from 'src/plugins/navigation/public'; @@ -33,6 +35,7 @@ import { import { APP_NAME, VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from './breadcrumbs'; import { EmbeddableStateTransfer } from '../../../../embeddable/public'; +import { VISUALIZE_APP_LOCATOR, VisualizeLocatorParams } from '../../../common/locator'; interface VisualizeCapabilities { createShortUrl: boolean; @@ -95,6 +98,7 @@ export const getTopNavConfig = ( savedObjectsTagging, presentationUtil, usageCollection, + getKibanaVersion, }: VisualizeServices ) => { const { vis, embeddableHandler } = visInstance; @@ -279,6 +283,22 @@ export const getTopNavConfig = ( testId: 'shareTopNavButton', run: (anchorElement) => { if (share && !embeddableId) { + const currentState = stateContainer.getState(); + const searchParams = parse(history.location.search); + const params: VisualizeLocatorParams = { + visId: savedVis?.id, + filters: currentState.filters, + refreshInterval: undefined, + timeRange: data.query.timefilter.timefilter.getTime(), + uiState: currentState.uiState, + query: currentState.query, + vis: currentState.vis, + linked: currentState.linked, + indexPattern: + visInstance.savedSearch?.searchSource?.getField('index')?.id ?? + (searchParams.indexPattern as string), + savedSearchId: visInstance.savedSearch?.id ?? (searchParams.savedSearchId as string), + }; // TODO: support sharing in by-value mode share.toggleShareContextMenu({ anchorElement, @@ -288,7 +308,17 @@ export const getTopNavConfig = ( objectId: savedVis?.id, objectType: 'visualization', sharingData: { - title: savedVis?.title, + title: + savedVis?.title || + i18n.translate('visualize.reporting.defaultReportTitle', { + defaultMessage: 'Visualization [{date}]', + values: { date: moment().toISOString(true) }, + }), + locatorParams: { + id: VISUALIZE_APP_LOCATOR, + version: getKibanaVersion(), + params, + }, }, isDirty: hasUnappliedChanges || hasUnsavedChanges, showPublicUrlSwitch, diff --git a/src/plugins/visualize/public/application/utils/stubs.ts b/src/plugins/visualize/public/application/utils/stubs.ts index 41a017306dc0a..086811df02baa 100644 --- a/src/plugins/visualize/public/application/utils/stubs.ts +++ b/src/plugins/visualize/public/application/utils/stubs.ts @@ -27,7 +27,6 @@ export const visualizeAppStateStub: VisualizeAppState = { { id: '1', enabled: true, - // @ts-expect-error type: 'avg', schema: 'metric', params: { field: 'total_quantity', customLabel: 'average items' }, diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts index b101a3c2feae9..26f866d22ce4e 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts @@ -157,7 +157,6 @@ describe('useVisualizeAppState', () => { }; it('should successfully update vis state and set up app state container', async () => { - // @ts-expect-error stateContainerGetStateMock.mockImplementation(() => state); const { result, waitForNextUpdate } = renderHook(() => useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) @@ -204,7 +203,6 @@ describe('useVisualizeAppState', () => { it(`should add warning toast and redirect to the landing page if setting new vis state was not successful, e.x. invalid query params`, async () => { - // @ts-expect-error stateContainerGetStateMock.mockImplementation(() => state); // @ts-expect-error savedVisInstance.vis.setState.mockRejectedValue({ diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index 6e901882a9365..19327ac940e9d 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -6,15 +6,4 @@ * Side Public License, v 1. */ -export const APP_NAME = 'visualize'; - -export const VisualizeConstants = { - VISUALIZE_BASE_PATH: '/app/visualize', - LANDING_PAGE_PATH: '/', - WIZARD_STEP_1_PAGE_PATH: '/new', - WIZARD_STEP_2_PAGE_PATH: '/new/configure', - CREATE_PATH: '/create', - EDIT_PATH: '/edit', - EDIT_BY_VALUE_PATH: '/edit_by_value', - APP_ID: 'visualize', -}; +export { VisualizeConstants, APP_NAME } from '../../common/constants'; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 00c3545034b32..d71e7fd81f1d9 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -47,6 +47,7 @@ import type { UsageCollectionStart } from '../../usage_collection/public'; import { setVisEditorsRegistry, setUISettings, setUsageCollector } from './services'; import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry'; +import { VisualizeLocatorDefinition } from '../common/locator'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; @@ -92,7 +93,7 @@ export class VisualizePlugin public setup( core: CoreSetup, - { home, urlForwarding, data }: VisualizePluginSetupDependencies + { home, urlForwarding, data, share }: VisualizePluginSetupDependencies ) { const { appMounted, @@ -209,6 +210,7 @@ export class VisualizePlugin savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(), presentationUtil: pluginsStart.presentationUtil, usageCollection: pluginsStart.usageCollection, + getKibanaVersion: () => this.initializerContext.env.packageInfo.version, }; params.element.classList.add('visAppWrapper'); @@ -241,6 +243,10 @@ export class VisualizePlugin }); } + if (share) { + share.url.locators.create(new VisualizeLocatorDefinition()); + } + return { visEditorsRegistry: this.visEditorsRegistry, } as VisualizePluginSetup; diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index 4dcf43dadf8ba..3f1f7487085bf 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -6,11 +6,7 @@ "declaration": true, "declarationMap": true }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*" - ], + "include": ["common/**/*", "public/**/*", "server/**/*", "../../../typings/**/*"], "references": [ { "path": "../../core/tsconfig.json" }, { "path": "../data/tsconfig.json" }, @@ -28,6 +24,6 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../home/tsconfig.json" }, { "path": "../presentation_util/tsconfig.json" }, - { "path": "../discover/tsconfig.json" }, + { "path": "../discover/tsconfig.json" } ] } diff --git a/test/api_integration/apis/status/status.js b/test/api_integration/apis/status/status.js index 22076b2cddbc5..e1545c448fce8 100644 --- a/test/api_integration/apis/status/status.js +++ b/test/api_integration/apis/status/status.js @@ -25,9 +25,10 @@ export default function ({ getService }) { expect(body.version.build_number).to.be.a('number'); expect(body.status.overall).to.be.an('object'); - expect(body.status.overall.state).to.be('green'); + expect(body.status.overall.level).to.be('available'); - expect(body.status.statuses).to.be.an('array'); + expect(body.status.core).to.be.an('object'); + expect(body.status.plugins).to.be.an('object'); expect(body.metrics.collection_interval_in_millis).to.be.a('number'); diff --git a/test/common/services/es_archiver.ts b/test/common/services/es_archiver.ts index d99a2e8d10236..2ea4b6ce3a434 100644 --- a/test/common/services/es_archiver.ts +++ b/test/common/services/es_archiver.ts @@ -8,8 +8,6 @@ import { EsArchiver } from '@kbn/es-archiver'; import { FtrProviderContext } from '../ftr_provider_context'; - -// @ts-ignore not TS yet import * as KibanaServer from './kibana_server'; export function EsArchiverProvider({ getService }: FtrProviderContext): EsArchiver { diff --git a/test/functional_execution_context/config.ts b/test/functional_execution_context/config.ts new file mode 100644 index 0000000000000..6e46189073001 --- /dev/null +++ b/test/functional_execution_context/config.ts @@ -0,0 +1,43 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; +import Path from 'path'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + ...functionalConfig.getAll(), + rootTags: ['skipCloud'], + testFiles: [require.resolve('./tests/')], + junit: { + reportName: 'Execution Context Functional Tests', + }, + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + '--execution_context.enabled=true', + '--logging.appenders.file.type=file', + `--logging.appenders.file.fileName=${Path.resolve(__dirname, './kibana.log')}`, + '--logging.appenders.file.layout.type=json', + + '--logging.loggers[0].name=elasticsearch.query', + '--logging.loggers[0].level=all', + // eslint-disable-next-line prettier/prettier + '--logging.loggers[0].appenders=[\"file\"]', + + '--logging.loggers[1].name=execution_context', + '--logging.loggers[1].level=debug', + // eslint-disable-next-line prettier/prettier + '--logging.loggers[1].appenders=[\"file\"]', + ], + }, + }; +} diff --git a/test/functional_execution_context/ftr_provider_context.ts b/test/functional_execution_context/ftr_provider_context.ts new file mode 100644 index 0000000000000..d4ac701735efb --- /dev/null +++ b/test/functional_execution_context/ftr_provider_context.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; +import { pageObjects } from '../functional/page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { pageObjects }; diff --git a/test/functional_execution_context/services.ts b/test/functional_execution_context/services.ts new file mode 100644 index 0000000000000..b0cf94fedd749 --- /dev/null +++ b/test/functional_execution_context/services.ts @@ -0,0 +1,11 @@ +/* + * 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 { services as functionalServices } from '../functional/services'; + +export const services = functionalServices; diff --git a/test/functional_execution_context/tests/execution_context.ts b/test/functional_execution_context/tests/execution_context.ts new file mode 100644 index 0000000000000..ad9b4332c9f02 --- /dev/null +++ b/test/functional_execution_context/tests/execution_context.ts @@ -0,0 +1,374 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { Ecs, KibanaExecutionContext } from 'kibana/server'; + +import Fs from 'fs/promises'; +import Path from 'path'; +import { isEqual } from 'lodash'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +const logFilePath = Path.resolve(__dirname, '../kibana.log'); + +// to avoid splitting log record containing \n symbol +const endOfLine = /(?<=})\s*\n/; +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home']); + const retry = getService('retry'); + + async function assertLogContains( + description: string, + predicate: (record: Ecs) => boolean + ): Promise { + // logs are written to disk asynchronously. I sacrificed performance to reduce flakiness. + await retry.waitFor(description, async () => { + const logsStr = await Fs.readFile(logFilePath, 'utf-8'); + const normalizedRecords = logsStr + .split(endOfLine) + .filter(Boolean) + .map((s) => JSON.parse(s)); + + return normalizedRecords.some(predicate); + }); + } + + function isExecutionContextLog( + record: string | undefined, + executionContext: KibanaExecutionContext + ) { + if (!record) return false; + try { + const object = JSON.parse(record); + return isEqual(object, executionContext); + } catch (e) { + return false; + } + } + + describe('Execution context service', () => { + before(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.removeSampleDataSet('flights'); + }); + + describe('discover app', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + it('propagates context for Discover', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => Boolean(record.http?.request?.id?.includes('kibana:application:discover')) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + description: 'fetch documents', + id: '', + name: 'discover', + type: 'application', + // discovery doesn't have an URL since one of from the example dataset is not saved separately + url: '/app/discover', + }) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + description: 'fetch chart data and total hits', + id: '', + name: 'discover', + type: 'application', + url: '/app/discover', + }) + ); + }); + }); + + describe('dashboard app', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + describe('propagates context for Lens visualizations', () => { + it('lnsXY', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsXY', + id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', + description: '[Flights] Flight count', + url: '/app/lens#/edit_by_value', + }) + ); + }); + + it('lnsMetric', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsMetric', + id: '2e33ade5-96e5-40b4-b460-493e5d4fa834', + description: '', + url: '/app/lens#/edit_by_value', + }) + ); + }); + + it('lnsDatatable', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsDatatable', + id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', + description: 'Cities by delay, cancellation', + url: '/app/lens#/edit_by_value', + }) + ); + }); + + it('lnsPie', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' + ) + ) + ); + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsPie', + id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', + description: '[Flights] Delay Type', + url: '/app/lens#/edit_by_value', + }) + ); + }); + }); + + it('propagates context for built-in Discover', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' + ) + ) + ); + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'search', + name: 'discover', + id: '571aaf70-4c88-11e8-b3d7-01146121b73d', + description: '[Flights] Flight Log', + url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', + }) + ); + }); + + it('propagates context for TSVB visualizations', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:TSVB:bcb63b50-4c89-11e8-b3d7-01146121b73d' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'TSVB', + id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', + description: '[Flights] Delays & Cancellations', + url: '/app/visualize#/edit/bcb63b50-4c89-11e8-b3d7-01146121b73d', + }) + ); + }); + + it('propagates context for Vega visualizations', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vega:ed78a660-53a0-11e8-acbd-0be0ad9d822b' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Vega', + id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', + description: '[Flights] Airport Connections (Hover Over Airport)', + url: '/app/visualize#/edit/ed78a660-53a0-11e8-acbd-0be0ad9d822b', + }) + ); + }); + + it('propagates context for Tag Cloud visualization', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Tag cloud:293b5a30-4c8f-11e8-b3d7-01146121b73d' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Tag cloud', + id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', + description: '[Flights] Destination Weather', + url: '/app/visualize#/edit/293b5a30-4c8f-11e8-b3d7-01146121b73d', + }) + ); + }); + + it('propagates context for Vertical bar visualization', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vertical bar:9886b410-4c8b-11e8-b3d7-01146121b73d' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Vertical bar', + id: '9886b410-4c8b-11e8-b3d7-01146121b73d', + description: '[Flights] Delay Buckets', + url: '/app/visualize#/edit/9886b410-4c8b-11e8-b3d7-01146121b73d', + }) + ); + }); + }); + }); +} diff --git a/test/functional_execution_context/tests/index.ts b/test/functional_execution_context/tests/index.ts new file mode 100644 index 0000000000000..6dc92f6fb3c8b --- /dev/null +++ b/test/functional_execution_context/tests/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Execution context', function () { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./execution_context')); + }); +} diff --git a/test/package/templates/kibana.yml b/test/package/templates/kibana.yml index a5e44b7acb018..ac2d03467051b 100644 --- a/test/package/templates/kibana.yml +++ b/test/package/templates/kibana.yml @@ -3,3 +3,6 @@ server.host: 0.0.0.0 elasticsearch.hosts: http://192.168.50.1:9200 elasticsearch.username: "{{ elasticsearch_username }}" elasticsearch.password: "{{ elasticsearch_password }}" + +pid.file: /run/kibana/kibana.pid +logging.dest: /var/log/kibana/kibana.log \ No newline at end of file diff --git a/test/plugin_functional/test_suites/core_plugins/status.ts b/test/plugin_functional/test_suites/core_plugins/status.ts index 2b0f15cb39273..10ca8c6722046 100644 --- a/test/plugin_functional/test_suites/core_plugins/status.ts +++ b/test/plugin_functional/test_suites/core_plugins/status.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); const getStatus = async (pluginName?: string) => { - const resp = await supertest.get('/api/status?v8format=true'); + const resp = await supertest.get('/api/status'); if (pluginName) { return resp.body.status.plugins[pluginName]; diff --git a/test/server_integration/http/platform/status.ts b/test/server_integration/http/platform/status.ts index 0dcf82c9bea9e..e443ce3f31cbf 100644 --- a/test/server_integration/http/platform/status.ts +++ b/test/server_integration/http/platform/status.ts @@ -18,7 +18,7 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const getStatus = async (pluginName: string): Promise => { - const resp = await supertest.get('/api/status?v8format=true'); + const resp = await supertest.get('/api/status'); return resp.body.status.plugins[pluginName]; }; diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index 7825cbfb45f37..d3abfca83c8e8 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -13,3 +13,4 @@ export * from './alert_history_schema'; export * from './rewrite_request_case'; export const BASE_ACTION_API_PATH = '/api/actions'; +export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 8e9ea1c5e4aa9..450bf1744150d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -52,7 +52,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('config validation', () => { - test('config validation succeeds when config is valid', () => { + test('config validation succeeds when config is valid for nodemailer well known service', () => { const config: Record = { service: 'gmail', from: 'bob@example.com', @@ -64,14 +64,46 @@ describe('config validation', () => { port: null, secure: null, }); + }); + + test(`config validation succeeds when config is valid and defaults to 'other' when service is undefined`, () => { + const config: Record = { + from: 'bob@example.com', + host: 'elastic.co', + port: 8080, + hasAuth: true, + }; + expect(validateConfig(actionType, config)).toEqual({ + ...config, + service: 'other', + secure: null, + }); + }); + + test(`config validation succeeds when config is valid and service requires custom host/port value`, () => { + const config: Record = { + service: 'exchange_server', + from: 'bob@example.com', + host: 'elastic.co', + port: 8080, + hasAuth: true, + }; + expect(validateConfig(actionType, config)).toEqual({ + ...config, + secure: null, + }); + }); - delete config.service; - config.host = 'elastic.co'; - config.port = 8080; - config.hasAuth = true; + test(`config validation succeeds when config is valid and service is elastic_cloud`, () => { + const config: Record = { + service: 'elastic_cloud', + from: 'bob@example.com', + hasAuth: true, + }; expect(validateConfig(actionType, config)).toEqual({ ...config, - service: null, + host: null, + port: null, secure: null, }); }); @@ -325,7 +357,7 @@ describe('execute()', () => { ...executorOptions, config: { ...config, - service: null, + service: 'other', hasAuth: false, }, secrets: { @@ -381,12 +413,73 @@ describe('execute()', () => { `); }); + test('parameters are as expected when using elastic_cloud service', async () => { + const customExecutorOptions: EmailActionTypeExecutorOptions = { + ...executorOptions, + config: { + ...config, + service: 'elastic_cloud', + hasAuth: false, + }, + secrets: { + ...secrets, + user: null, + password: null, + }, + }; + + sendEmailMock.mockReset(); + await actionType.executor(customExecutorOptions); + expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "configurationUtilities": Object { + "ensureActionTypeEnabled": [MockFunction], + "ensureHostnameAllowed": [MockFunction], + "ensureUriAllowed": [MockFunction], + "getCustomHostSettings": [MockFunction], + "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], + "getSSLSettings": [MockFunction], + "isActionTypeEnabled": [MockFunction], + "isHostnameAllowed": [MockFunction], + "isUriAllowed": [MockFunction], + }, + "content": Object { + "message": "a message to you + + -- + + This message was sent by Kibana.", + "subject": "the subject", + }, + "hasAuth": false, + "routing": Object { + "bcc": Array [ + "jimmy@example.com", + ], + "cc": Array [ + "james@example.com", + ], + "from": "bob@example.com", + "to": Array [ + "jim@example.com", + ], + }, + "transport": Object { + "host": "dockerhost", + "port": 10025, + "secure": false, + }, + } + `); + }); + test('returns expected result when an error is thrown', async () => { const customExecutorOptions: EmailActionTypeExecutorOptions = { ...executorOptions, config: { ...config, - service: null, + service: 'other', hasAuth: false, }, secrets: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 47748f0f13722..9b11aec6251f6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -9,6 +9,7 @@ import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import nodemailerGetService from 'nodemailer/lib/well-known'; +import SMTPConnection from 'nodemailer/lib/smtp-connection'; import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email'; import { portSchema } from './lib/schemas'; @@ -32,10 +33,29 @@ export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions< // config definition export type ActionTypeConfigType = TypeOf; +// supported values for `service` in addition to nodemailer's list of well-known services +export enum AdditionalEmailServices { + ELASTIC_CLOUD = 'elastic_cloud', + EXCHANGE = 'exchange_server', + OTHER = 'other', +} + +// these values for `service` require users to fill in host/port/secure +export const CUSTOM_CONFIG_SERVICES: string[] = [ + AdditionalEmailServices.EXCHANGE, + AdditionalEmailServices.OTHER, +]; + +export const ELASTIC_CLOUD_SERVICE: SMTPConnection.Options = { + host: 'dockerhost', + port: 10025, + secure: false, +}; + const EMAIL_FOOTER_DIVIDER = '\n\n--\n\n'; const ConfigSchemaProps = { - service: schema.nullable(schema.string()), + service: schema.string({ defaultValue: 'other' }), host: schema.nullable(schema.string()), port: schema.nullable(portSchema()), secure: schema.nullable(schema.boolean()), @@ -58,7 +78,8 @@ function validateConfig( // translate messages. if (config.service === JSON_TRANSPORT_SERVICE) { return; - } else if (config.service == null) { + } else if (CUSTOM_CONFIG_SERVICES.indexOf(config.service) >= 0) { + // If configured `service` requires custom host/port/secure settings, validate that they are set if (config.host == null && config.port == null) { return 'either [service] or [host]/[port] is required'; } @@ -75,6 +96,7 @@ function validateConfig( return `[host] value '${config.host}' is not in the allowedHosts configuration`; } } else { + // Check configured `service` against nodemailer list of well known services + any custom ones allowed by Kibana const host = getServiceNameHost(config.service); if (host == null) { return `[service] value '${config.service}' is not valid`; @@ -201,13 +223,20 @@ async function executor( transport.password = secrets.password; } - if (config.service !== null) { - transport.service = config.service; - } else { + if (CUSTOM_CONFIG_SERVICES.indexOf(config.service) >= 0) { + // use configured host/port/secure values // already validated service or host/port is not null ... transport.host = config.host!; transport.port = config.port!; transport.secure = getSecureValue(config.secure, config.port); + } else if (config.service === AdditionalEmailServices.ELASTIC_CLOUD) { + // use custom elastic cloud settings + transport.host = ELASTIC_CLOUD_SERVICE.host!; + transport.port = ELASTIC_CLOUD_SERVICE.port!; + transport.secure = ELASTIC_CLOUD_SERVICE.secure!; + } else { + // use nodemailer's well known service config + transport.service = config.service; } const footerMessage = getFooterMessage({ @@ -253,6 +282,10 @@ async function executor( // utilities function getServiceNameHost(service: string): string | null { + if (service === AdditionalEmailServices.ELASTIC_CLOUD) { + return ELASTIC_CLOUD_SERVICE.host!; + } + const serviceEntry = nodemailerGetService(service); if (serviceEntry === false) return null; diff --git a/x-pack/plugins/actions/server/routes/get_well_known_email_service.test.ts b/x-pack/plugins/actions/server/routes/get_well_known_email_service.test.ts new file mode 100644 index 0000000000000..bbcedf18142ef --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_well_known_email_service.test.ts @@ -0,0 +1,175 @@ +/* + * 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 { getWellKnownEmailServiceRoute } from './get_well_known_email_service'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; +import { verifyAccessAndContext } from './verify_access_and_context'; + +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); +}); + +describe('getWellKnownEmailServiceRoute', () => { + it('returns config for well known email service', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + expect(config.path).toMatchInlineSnapshot( + `"/internal/actions/connector/_email_config/{service}"` + ); + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'gmail' }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "host": "smtp.gmail.com", + "port": 465, + "secure": true, + }, + } + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + host: 'smtp.gmail.com', + port: 465, + secure: true, + }, + }); + }); + + it('returns config for elastic cloud email service', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + expect(config.path).toMatchInlineSnapshot( + `"/internal/actions/connector/_email_config/{service}"` + ); + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'elastic_cloud' }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "host": "dockerhost", + "port": 10025, + "secure": false, + }, + } + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + host: 'dockerhost', + port: 10025, + secure: false, + }, + }); + }); + + it('returns empty for unknown service', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + expect(config.path).toMatchInlineSnapshot( + `"/internal/actions/connector/_email_config/{service}"` + ); + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'foo' }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object {}, + } + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: {}, + }); + }); + + it('ensures the license allows getting well known email service config', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'gmail' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); + + it('ensures the license check prevents getting well known email service config', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { + throw new Error('OMG'); + }); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'gmail' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/get_well_known_email_service.ts b/x-pack/plugins/actions/server/routes/get_well_known_email_service.ts new file mode 100644 index 0000000000000..837084f43b864 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_well_known_email_service.ts @@ -0,0 +1,57 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import nodemailerGetService from 'nodemailer/lib/well-known'; +import SMTPConnection from 'nodemailer/lib/smtp-connection'; +import { ILicenseState } from '../lib'; +import { INTERNAL_BASE_ACTION_API_PATH } from '../../common'; +import { ActionsRequestHandlerContext } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { AdditionalEmailServices, ELASTIC_CLOUD_SERVICE } from '../builtin_action_types/email'; + +const paramSchema = schema.object({ + service: schema.string(), +}); + +export const getWellKnownEmailServiceRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ACTION_API_PATH}/connector/_email_config/{service}`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const { service } = req.params; + + let response: SMTPConnection.Options = {}; + if (service === AdditionalEmailServices.ELASTIC_CLOUD) { + response = ELASTIC_CLOUD_SERVICE; + } else { + const serviceEntry = nodemailerGetService(service); + if (serviceEntry) { + response = { + host: serviceEntry.host, + port: serviceEntry.port, + secure: serviceEntry.secure, + }; + } + } + + return res.ok({ + body: response, + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index a236e514ef78d..0d39d87635d5e 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -15,6 +15,7 @@ import { getActionRoute } from './get'; import { getAllActionRoute } from './get_all'; import { connectorTypesRoute } from './connector_types'; import { updateActionRoute } from './update'; +import { getWellKnownEmailServiceRoute } from './get_well_known_email_service'; import { defineLegacyRoutes } from './legacy'; export function defineRoutes( @@ -30,4 +31,6 @@ export function defineRoutes( updateActionRoute(router, licenseState); connectorTypesRoute(router, licenseState); executeActionRoute(router, licenseState); + + getWellKnownEmailServiceRoute(router, licenseState); } diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts index 7dc1426c13a4b..c094109a43d97 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts @@ -119,6 +119,54 @@ describe('successful migrations', () => { }); }); + describe('7.16.0', () => { + test('set service config property for .email connectors if service is undefined', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockDataForEmail({ config: { service: undefined } }); + const migratedAction = migration716(action, context); + expect(migratedAction.attributes.config).toEqual({ + service: 'other', + }); + expect(migratedAction).toEqual({ + ...action, + attributes: { + ...action.attributes, + config: { + service: 'other', + }, + }, + }); + }); + + test('set service config property for .email connectors if service is null', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockDataForEmail({ config: { service: null } }); + const migratedAction = migration716(action, context); + expect(migratedAction.attributes.config).toEqual({ + service: 'other', + }); + expect(migratedAction).toEqual({ + ...action, + attributes: { + ...action.attributes, + config: { + service: 'other', + }, + }, + }); + }); + + test('skips migrating .email connectors if service is defined, even if value is nonsense', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockDataForEmail({ config: { service: 'gobbledygook' } }); + const migratedAction = migration716(action, context); + expect(migratedAction.attributes.config).toEqual({ + service: 'gobbledygook', + }); + expect(migratedAction).toEqual(action); + }); + }); + describe('8.0.0', () => { test('no op migration for rules SO', () => { const migration800 = getActionsMigrations(encryptedSavedObjectsSetup)['8.0.0']; diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts index 7857a9e1f833f..e75f3eb41f2df 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts @@ -62,6 +62,12 @@ export function getActionsMigrations( pipeMigrations(addisMissingSecretsField) ); + const migrationEmailActionsSixteen = createEsoMigration( + encryptedSavedObjects, + (doc): doc is SavedObjectUnsanitizedDoc => doc.attributes.actionTypeId === '.email', + pipeMigrations(setServiceConfigIfNotSet) + ); + const migrationActions800 = createEsoMigration( encryptedSavedObjects, (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => @@ -73,6 +79,7 @@ export function getActionsMigrations( '7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'), '7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'), + '7.16.0': executeMigrationWithErrorHandling(migrationEmailActionsSixteen, '7.16.0'), '8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'), }; } @@ -157,6 +164,24 @@ const addHasAuthConfigurationObject = ( }; }; +const setServiceConfigIfNotSet = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc => { + if (doc.attributes.actionTypeId !== '.email' || null != doc.attributes.config.service) { + return doc; + } + return { + ...doc, + attributes: { + ...doc.attributes, + config: { + ...doc.attributes.config, + service: 'other', + }, + }, + }; +}; + const addisMissingSecretsField = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc => { diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 5a2a124f55abc..c3e21e02cdb7d 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -35,6 +35,9 @@ import { AlertNotifyWhenType, AlertTypeParams, ResolvedSanitizedRule, + AlertWithLegacyId, + SanitizedAlertWithLegacyId, + PartialAlertWithLegacyId, } from '../types'; import { validateAlertTypeParams, @@ -383,9 +386,11 @@ export class RulesClient { public async get({ id, + includeLegacyId = false, }: { id: string; - }): Promise> { + includeLegacyId?: boolean; + }): Promise | SanitizedAlertWithLegacyId> { const result = await this.unsecuredSavedObjectsClient.get('alert', id); try { await this.authorization.ensureAuthorized({ @@ -414,7 +419,8 @@ export class RulesClient { result.id, result.attributes.alertTypeId, result.attributes, - result.references + result.references, + includeLegacyId ); } @@ -486,7 +492,8 @@ export class RulesClient { dateStart, }: GetAlertInstanceSummaryParams): Promise { this.logger.debug(`getAlertInstanceSummary(): getting alert ${id}`); - const alert = await this.get({ id }); + const alert = (await this.get({ id, includeLegacyId: true })) as SanitizedAlertWithLegacyId; + await this.authorization.ensureAuthorized({ ruleTypeId: alert.alertTypeId, consumer: alert.consumer, @@ -505,13 +512,18 @@ export class RulesClient { this.logger.debug(`getAlertInstanceSummary(): search the event log for alert ${id}`); let events: IEvent[]; try { - const queryResults = await eventLogClient.findEventsBySavedObjectIds('alert', [id], { - page: 1, - per_page: 10000, - start: parsedDateStart.toISOString(), - end: dateNow.toISOString(), - sort_order: 'desc', - }); + const queryResults = await eventLogClient.findEventsBySavedObjectIds( + 'alert', + [id], + { + page: 1, + per_page: 10000, + start: parsedDateStart.toISOString(), + end: dateNow.toISOString(), + sort_order: 'desc', + }, + alert.legacyId !== null ? [alert.legacyId] : undefined + ); events = queryResults.data; } catch (err) { this.logger.debug( @@ -1533,13 +1545,26 @@ export class RulesClient { id: string, ruleTypeId: string, rawAlert: RawAlert, - references: SavedObjectReference[] | undefined - ): Alert { + references: SavedObjectReference[] | undefined, + includeLegacyId: boolean = false + ): Alert | AlertWithLegacyId { const ruleType = this.ruleTypeRegistry.get(ruleTypeId); // In order to support the partial update API of Saved Objects we have to support // partial updates of an Alert, but when we receive an actual RawAlert, it is safe // to cast the result to an Alert - return this.getPartialAlertFromRaw(id, ruleType, rawAlert, references) as Alert; + const res = this.getPartialAlertFromRaw( + id, + ruleType, + rawAlert, + references, + includeLegacyId + ); + // include to result because it is for internal rules client usage + if (includeLegacyId) { + return res as AlertWithLegacyId; + } + // exclude from result because it is an internal variable + return omit(res, ['legacyId']) as Alert; } private getPartialAlertFromRaw( @@ -1550,17 +1575,18 @@ export class RulesClient { updatedAt, meta, notifyWhen, + legacyId, scheduledTaskId, params, - legacyId, // exclude from result because it is an internal variable executionStatus, schedule, actions, ...partialRawAlert }: Partial, - references: SavedObjectReference[] | undefined - ): PartialAlert { - return { + references: SavedObjectReference[] | undefined, + includeLegacyId: boolean = false + ): PartialAlert | PartialAlertWithLegacyId { + const rule = { id, notifyWhen, ...partialRawAlert, @@ -1576,6 +1602,9 @@ export class RulesClient { ? { executionStatus: alertExecutionStatusFromRaw(this.logger, id, executionStatus) } : {}), }; + return includeLegacyId + ? ({ ...rule, legacyId } as PartialAlertWithLegacyId) + : (rule as PartialAlert); } private async validateActions( diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts index f8414b08f191b..6b018fb342d99 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts @@ -212,6 +212,7 @@ describe('getAlertInstanceSummary()', () => { "sort_order": "desc", "start": "2019-02-12T21:00:22.479Z", }, + undefined, ] `); // calculate the expected start/end date for one test @@ -225,6 +226,38 @@ describe('getAlertInstanceSummary()', () => { expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); }); + test('calls event log client with legacy ids param', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce( + getAlertInstanceSummarySavedObject({ legacyId: '99999' }) + ); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + await rulesClient.getAlertInstanceSummary({ id: '1' }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + Array [ + "1", + ], + Object { + "end": "2019-02-12T21:01:22.479Z", + "page": 1, + "per_page": 10000, + "sort_order": "desc", + "start": "2019-02-12T21:00:22.479Z", + }, + Array [ + "99999", + ], + ] + `); + }); + test('calls event log client with start date', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 67565271fedc8..ba35890efd781 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -192,6 +192,21 @@ export interface RawAlertExecutionStatus extends SavedObjectAttributes { export type PartialAlert = Pick, 'id'> & Partial, 'id'>>; +export interface AlertWithLegacyId extends Alert { + legacyId: string | null; +} + +export type SanitizedAlertWithLegacyId = Omit< + AlertWithLegacyId, + 'apiKey' +>; + +export type PartialAlertWithLegacyId = Pick< + AlertWithLegacyId, + 'id' +> & + Partial, 'id'>>; + export interface RawAlert extends SavedObjectAttributes { enabled: boolean; name: string; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 72491a2bc1e31..9cbb13f7227a3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -203,7 +203,8 @@ export const AllCasesGeneric = React.memo( handleIsLoading, isLoadingCases: loading, refreshCases, - showActions, + // isSelectorView is boolean | undefined. We need to convert it to a boolean. + isSelectorView: !!isSelectorView, userCanCrud, connectors, }); diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 8b755b0c60968..c0bd6536f1b73 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -72,7 +72,7 @@ export interface GetCasesColumn { handleIsLoading: (a: boolean) => void; isLoadingCases: string[]; refreshCases?: (a?: boolean) => void; - showActions: boolean; + isSelectorView: boolean; userCanCrud: boolean; connectors?: ActionConnector[]; } @@ -84,7 +84,7 @@ export const useCasesColumns = ({ handleIsLoading, isLoadingCases, refreshCases, - showActions, + isSelectorView, userCanCrud, connectors = [], }: GetCasesColumn): CasesColumns[] => { @@ -281,38 +281,42 @@ export const useCasesColumns = ({ return getEmptyTagValue(); }, }, - { - name: i18n.STATUS, - render: (theCase: Case) => { - if (theCase?.subCases == null || theCase.subCases.length === 0) { - if (theCase.status == null || theCase.type === CaseType.collection) { - return getEmptyTagValue(); - } - return ( - 0} - onStatusChanged={(status) => - handleDispatchUpdate({ - updateKey: 'status', - updateValue: status, - caseId: theCase.id, - version: theCase.version, - }) + ...(!isSelectorView + ? [ + { + name: i18n.STATUS, + render: (theCase: Case) => { + if (theCase?.subCases == null || theCase.subCases.length === 0) { + if (theCase.status == null || theCase.type === CaseType.collection) { + return getEmptyTagValue(); + } + return ( + 0} + onStatusChanged={(status) => + handleDispatchUpdate({ + updateKey: 'status', + updateValue: status, + caseId: theCase.id, + version: theCase.version, + }) + } + /> + ); } - /> - ); - } - const badges = getSubCasesStatusCountsBadges(theCase.subCases); - return badges.map(({ color, count }, index) => ( - - {count} - - )); - }, - }, - ...(showActions + const badges = getSubCasesStatusCountsBadges(theCase.subCases); + return badges.map(({ color, count }, index) => ( + + {count} + + )); + }, + }, + ] + : []), + ...(userCanCrud && !isSelectorView ? [ { name: ( diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 9e6928d43c862..3fff43108772d 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -144,7 +144,7 @@ describe('AllCasesGeneric', () => { filterStatus: CaseStatuses.open, handleIsLoading: jest.fn(), isLoadingCases: [], - showActions: true, + isSelectorView: false, userCanCrud: true, }; @@ -377,7 +377,7 @@ describe('AllCasesGeneric', () => { isLoadingCases: [], filterStatus: CaseStatuses.open, handleIsLoading: jest.fn(), - showActions: false, + isSelectorView: true, userCanCrud: true, }) ); @@ -926,4 +926,27 @@ describe('AllCasesGeneric', () => { ).toBeFalsy(); }); }); + + it('should not render status when isSelectorView=true', async () => { + const wrapper = mount( + + + + ); + + const { result } = renderHook(() => + useCasesColumns({ + ...defaultColumnArgs, + isSelectorView: true, + }) + ); + + expect(result.current.find((i) => i.name === 'Status')).toBeFalsy(); + + await waitFor(() => { + expect(wrapper.find('[data-test-subj="cases-table"]').exists()).toBeTruthy(); + }); + + expect(wrapper.find('[data-test-subj="case-view-status-dropdown"]').exists()).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/discover_enhanced/common/index.ts b/x-pack/plugins/discover_enhanced/common/index.ts index 4ac13918c7ed3..99afdb5f30c6b 100644 --- a/x-pack/plugins/discover_enhanced/common/index.ts +++ b/x-pack/plugins/discover_enhanced/common/index.ts @@ -5,7 +5,4 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110900 -/* eslint-disable @kbn/eslint/no_export_all */ - -export * from './config'; +export type { Config } from './config'; diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 33c7fcabc9f46..f637187c1c180 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -271,6 +271,7 @@ Request Body: |Property|Description|Type| |---|---|---| |ids|The array ids of the saved object.|string array| +|legacyIds|The array legacy ids of the saved object. This filter applies to the rules creted in Kibana versions before 8.0.0.|string array| Response body: @@ -284,7 +285,8 @@ interface EventLogClient { findEventsBySavedObjectIds( type: string, ids: string[], - options?: Partial + options?: Partial, + legacyIds?: string[] ): Promise; } @@ -404,7 +406,8 @@ export interface IEventLogClient { findEventsBySavedObjectIds( type: string, ids: string[], - options?: Partial + options?: Partial, + legacyIds?: string[] ): Promise; } ``` diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index cd87bc8e6e18b..ef43b9081f9ec 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -338,16 +338,106 @@ describe('queryEventsBySavedObject', () => { }, }) ); - await clusterClientAdapter.queryEventsBySavedObjects( - 'index-name', - 'namespace', - 'saved-object-type', - ['saved-object-id'], - DEFAULT_OPTIONS - ); + await clusterClientAdapter.queryEventsBySavedObjects({ + index: 'index-name', + namespace: 'namespace', + type: 'saved-object-type', + ids: ['saved-object-id'], + findOptions: DEFAULT_OPTIONS, + }); const [query] = clusterClient.search.mock.calls[0]; - expect(query).toMatchInlineSnapshot(` + expect(query).toMatchInlineSnapshot( + { + body: { + from: 0, + query: { + bool: { + filter: [], + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, + { + term: { + 'kibana.saved_objects.type': { + value: 'saved-object-type', + }, + }, + }, + { + term: { + 'kibana.saved_objects.namespace': { + value: 'namespace', + }, + }, + }, + ], + }, + }, + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + terms: { + 'kibana.saved_objects.id': ['saved-object-id'], + }, + }, + ], + }, + }, + }, + }, + { + range: { + 'kibana.version': { + gte: '8.0.0', + }, + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + size: 10, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + index: 'index-name', + track_total_hits: true, + }, + ` Object { "body": Object { "from": 0, @@ -375,13 +465,6 @@ describe('queryEventsBySavedObject', () => { }, }, }, - Object { - "terms": Object { - "kibana.saved_objects.id": Array [ - "saved-object-id", - ], - }, - }, Object { "term": Object { "kibana.saved_objects.namespace": Object { @@ -394,6 +477,43 @@ describe('queryEventsBySavedObject', () => { }, }, }, + Object { + "bool": Object { + "should": Array [ + Object { + "bool": Object { + "must": Array [ + Object { + "nested": Object { + "path": "kibana.saved_objects", + "query": Object { + "bool": Object { + "must": Array [ + Object { + "terms": Object { + "kibana.saved_objects.id": Array [ + "saved-object-id", + ], + }, + }, + ], + }, + }, + }, + }, + Object { + "range": Object { + "kibana.version": Object { + "gte": "8.0.0", + }, + }, + }, + ], + }, + }, + ], + }, + }, ], }, }, @@ -409,7 +529,8 @@ describe('queryEventsBySavedObject', () => { "index": "index-name", "track_total_hits": true, } - `); + ` + ); }); test('should call cluster with proper arguments with default namespace', async () => { @@ -429,80 +550,106 @@ describe('queryEventsBySavedObject', () => { }, }) ); - await clusterClientAdapter.queryEventsBySavedObjects( - 'index-name', - undefined, - 'saved-object-type', - ['saved-object-id'], - DEFAULT_OPTIONS - ); + await clusterClientAdapter.queryEventsBySavedObjects({ + index: 'index-name', + namespace: undefined, + type: 'saved-object-type', + ids: ['saved-object-id'], + findOptions: DEFAULT_OPTIONS, + }); const [query] = clusterClient.search.mock.calls[0]; - expect(query).toMatchInlineSnapshot(` - Object { - "body": Object { - "from": 0, - "query": Object { - "bool": Object { - "filter": Array [], - "must": Array [ - Object { - "nested": Object { - "path": "kibana.saved_objects", - "query": Object { - "bool": Object { - "must": Array [ - Object { - "term": Object { - "kibana.saved_objects.rel": Object { - "value": "primary", - }, + expect(query).toMatchObject({ + body: { + from: 0, + query: { + bool: { + filter: [], + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', }, }, - Object { - "term": Object { - "kibana.saved_objects.type": Object { - "value": "saved-object-type", - }, + }, + { + term: { + 'kibana.saved_objects.type': { + value: 'saved-object-type', }, }, - Object { - "terms": Object { - "kibana.saved_objects.id": Array [ - "saved-object-id", - ], + }, + { + bool: { + must_not: { + exists: { + field: 'kibana.saved_objects.namespace', + }, }, }, - Object { - "bool": Object { - "must_not": Object { - "exists": Object { - "field": "kibana.saved_objects.namespace", + }, + ], + }, + }, + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + terms: { + 'kibana.saved_objects.id': ['saved-object-id'], + }, + }, + ], }, }, }, }, + { + range: { + 'kibana.version': { + gte: '8.0.0', + }, + }, + }, ], }, }, - }, + ], }, - ], - }, - }, - "size": 10, - "sort": Array [ - Object { - "@timestamp": Object { - "order": "asc", }, - }, - ], + ], + }, }, - "index": "index-name", - "track_total_hits": true, - } - `); + size: 10, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + index: 'index-name', + track_total_hits: true, + }); }); test('should call cluster with sort', async () => { @@ -522,13 +669,13 @@ describe('queryEventsBySavedObject', () => { }, }) ); - await clusterClientAdapter.queryEventsBySavedObjects( - 'index-name', - 'namespace', - 'saved-object-type', - ['saved-object-id'], - { ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' } - ); + await clusterClientAdapter.queryEventsBySavedObjects({ + index: 'index-name', + namespace: 'namespace', + type: 'saved-object-type', + ids: ['saved-object-id'], + findOptions: { ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' }, + }); const [query] = clusterClient.search.mock.calls[0]; expect(query).toMatchObject({ @@ -559,85 +706,111 @@ describe('queryEventsBySavedObject', () => { const start = '2020-07-08T00:52:28.350Z'; - await clusterClientAdapter.queryEventsBySavedObjects( - 'index-name', - 'namespace', - 'saved-object-type', - ['saved-object-id'], - { ...DEFAULT_OPTIONS, start } - ); + await clusterClientAdapter.queryEventsBySavedObjects({ + index: 'index-name', + namespace: 'namespace', + type: 'saved-object-type', + ids: ['saved-object-id'], + findOptions: { ...DEFAULT_OPTIONS, start }, + }); const [query] = clusterClient.search.mock.calls[0]; - expect(query).toMatchInlineSnapshot(` - Object { - "body": Object { - "from": 0, - "query": Object { - "bool": Object { - "filter": Array [], - "must": Array [ - Object { - "nested": Object { - "path": "kibana.saved_objects", - "query": Object { - "bool": Object { - "must": Array [ - Object { - "term": Object { - "kibana.saved_objects.rel": Object { - "value": "primary", - }, + expect(query).toMatchObject({ + body: { + from: 0, + query: { + bool: { + filter: [], + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', }, }, - Object { - "term": Object { - "kibana.saved_objects.type": Object { - "value": "saved-object-type", - }, + }, + { + term: { + 'kibana.saved_objects.type': { + value: 'saved-object-type', }, }, - Object { - "terms": Object { - "kibana.saved_objects.id": Array [ - "saved-object-id", - ], + }, + { + term: { + 'kibana.saved_objects.namespace': { + value: 'namespace', }, }, - Object { - "term": Object { - "kibana.saved_objects.namespace": Object { - "value": "namespace", + }, + ], + }, + }, + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + terms: { + 'kibana.saved_objects.id': ['saved-object-id'], + }, + }, + ], + }, + }, + }, + }, + { + range: { + 'kibana.version': { + gte: '8.0.0', }, }, }, ], }, }, - }, + ], }, - Object { - "range": Object { - "@timestamp": Object { - "gte": "2020-07-08T00:52:28.350Z", - }, + }, + { + range: { + '@timestamp': { + gte: '2020-07-08T00:52:28.350Z', }, }, - ], - }, - }, - "size": 10, - "sort": Array [ - Object { - "@timestamp": Object { - "order": "asc", }, - }, - ], + ], + }, }, - "index": "index-name", - "track_total_hits": true, - } - `); + size: 10, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + index: 'index-name', + track_total_hits: true, + }); }); test('supports optional date range', async () => { @@ -661,92 +834,163 @@ describe('queryEventsBySavedObject', () => { const start = '2020-07-08T00:52:28.350Z'; const end = '2020-07-08T00:00:00.000Z'; - await clusterClientAdapter.queryEventsBySavedObjects( - 'index-name', - 'namespace', - 'saved-object-type', - ['saved-object-id'], - { ...DEFAULT_OPTIONS, start, end } - ); + await clusterClientAdapter.queryEventsBySavedObjects({ + index: 'index-name', + namespace: 'namespace', + type: 'saved-object-type', + ids: ['saved-object-id'], + findOptions: { ...DEFAULT_OPTIONS, start, end }, + legacyIds: ['legacy-id'], + }); const [query] = clusterClient.search.mock.calls[0]; - expect(query).toMatchInlineSnapshot(` - Object { - "body": Object { - "from": 0, - "query": Object { - "bool": Object { - "filter": Array [], - "must": Array [ - Object { - "nested": Object { - "path": "kibana.saved_objects", - "query": Object { - "bool": Object { - "must": Array [ - Object { - "term": Object { - "kibana.saved_objects.rel": Object { - "value": "primary", - }, + expect(query).toMatchObject({ + body: { + from: 0, + query: { + bool: { + filter: [], + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', }, }, - Object { - "term": Object { - "kibana.saved_objects.type": Object { - "value": "saved-object-type", + }, + { + term: { + 'kibana.saved_objects.type': { + value: 'saved-object-type', + }, + }, + }, + { + term: { + 'kibana.saved_objects.namespace': { + value: 'namespace', + }, + }, + }, + ], + }, + }, + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + terms: { + 'kibana.saved_objects.id': ['saved-object-id'], + }, + }, + ], + }, }, }, }, - Object { - "terms": Object { - "kibana.saved_objects.id": Array [ - "saved-object-id", - ], + { + range: { + 'kibana.version': { + gte: '8.0.0', + }, }, }, - Object { - "term": Object { - "kibana.saved_objects.namespace": Object { - "value": "namespace", + ], + }, + }, + { + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + terms: { + 'kibana.saved_objects.id': ['legacy-id'], + }, + }, + ], + }, }, }, }, + { + bool: { + should: [ + { + range: { + 'kibana.version': { + lt: '8.0.0', + }, + }, + }, + { + bool: { + must_not: { + exists: { + field: 'kibana.version', + }, + }, + }, + }, + ], + }, + }, ], }, }, - }, + ], }, - Object { - "range": Object { - "@timestamp": Object { - "gte": "2020-07-08T00:52:28.350Z", - }, + }, + { + range: { + '@timestamp': { + gte: '2020-07-08T00:52:28.350Z', }, }, - Object { - "range": Object { - "@timestamp": Object { - "lte": "2020-07-08T00:00:00.000Z", - }, + }, + { + range: { + '@timestamp': { + lte: '2020-07-08T00:00:00.000Z', }, }, - ], - }, - }, - "size": 10, - "sort": Array [ - Object { - "@timestamp": Object { - "order": "asc", }, - }, - ], + ], + }, }, - "index": "index-name", - "track_total_hits": true, - } - `); + size: 10, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + index: 'index-name', + track_total_hits: true, + }); }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index a3a48c04370d8..47bd29cf4b08a 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -41,9 +41,20 @@ export interface QueryEventsBySavedObjectResult { data: IValidatedEvent[]; } +interface QueryOptionsEventsBySavedObjectFilter { + index: string; + namespace: string | undefined; + type: string; + ids: string[]; + findOptions: FindOptionsType; + legacyIds?: string[]; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type AliasAny = any; +const LEGACY_ID_CUTOFF_VERSION = '8.0.0'; + export class ClusterClientAdapter { private readonly logger: Logger; private readonly elasticsearchClientPromise: Promise; @@ -202,13 +213,12 @@ export class ClusterClientAdapter { + const { index, namespace, type, ids, findOptions, legacyIds } = queryOptions; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { page, per_page: perPage, start, end, sort_field, sort_order, filter } = findOptions; + const defaultNamespaceQuery = { bool: { must_not: { @@ -238,41 +248,125 @@ export class ClusterClientAdapter 0) { + shouldQuery.push({ + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + terms: { + // default maximum of 65,536 terms, configurable by index.max_terms_count + 'kibana.saved_objects.id': legacyIds, + }, + }, + ], }, }, - { - term: { - 'kibana.saved_objects.type': { - value: type, + }, + }, + { + bool: { + should: [ + { + range: { + 'kibana.version': { + lt: LEGACY_ID_CUTOFF_VERSION, + }, }, }, - }, - { - terms: { - // default maximum of 65,536 terms, configurable by index.max_terms_count - 'kibana.saved_objects.id': ids, + { + bool: { + must_not: { + exists: { + field: 'kibana.version', + }, + }, + }, }, - }, - // @ts-expect-error undefined is not assignable as QueryDslTermQuery value - namespaceQuery, - ], + ], + }, }, - }, + ], }, + }); + } + + musts.push({ + bool: { + should: shouldQuery, }, - ]; + }); + if (start) { musts.push({ range: { diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index 1f58592ea44fa..6babfef9bd8a8 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -111,21 +111,27 @@ describe('EventLogStart', () => { esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue(result); expect( - await eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id']) + await eventLogClient.findEventsBySavedObjectIds( + 'saved-object-type', + ['saved-object-id'], + undefined, + ['legacy-id'] + ) ).toEqual(result); - expect(esContext.esAdapter.queryEventsBySavedObjects).toHaveBeenCalledWith( - esContext.esNames.indexPattern, - undefined, - 'saved-object-type', - ['saved-object-id'], - { + expect(esContext.esAdapter.queryEventsBySavedObjects).toHaveBeenCalledWith({ + index: esContext.esNames.indexPattern, + namespace: undefined, + type: 'saved-object-type', + ids: ['saved-object-id'], + findOptions: { page: 1, per_page: 10, sort_field: '@timestamp', sort_order: 'asc', - } - ); + }, + legacyIds: ['legacy-id'], + }); }); test('fetches all events in time frame that reference the saved object', async () => { @@ -189,26 +195,32 @@ describe('EventLogStart', () => { const end = moment().add(1, 'days').toISOString(); expect( - await eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'], { - start, - end, - }) + await eventLogClient.findEventsBySavedObjectIds( + 'saved-object-type', + ['saved-object-id'], + { + start, + end, + }, + ['legacy-id'] + ) ).toEqual(result); - expect(esContext.esAdapter.queryEventsBySavedObjects).toHaveBeenCalledWith( - esContext.esNames.indexPattern, - undefined, - 'saved-object-type', - ['saved-object-id'], - { + expect(esContext.esAdapter.queryEventsBySavedObjects).toHaveBeenCalledWith({ + index: esContext.esNames.indexPattern, + namespace: undefined, + type: 'saved-object-type', + ids: ['saved-object-id'], + findOptions: { page: 1, per_page: 10, sort_field: '@timestamp', sort_order: 'asc', start, end, - } - ); + }, + legacyIds: ['legacy-id'], + }); }); test('validates that the start date is valid', async () => { diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 5214db1169deb..39b78296e3875 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -83,7 +83,8 @@ export class EventLogClient implements IEventLogClient { async findEventsBySavedObjectIds( type: string, ids: string[], - options?: Partial + options?: Partial, + legacyIds?: string[] ): Promise { const findOptions = findOptionsSchema.validate(options ?? {}); @@ -93,12 +94,13 @@ export class EventLogClient implements IEventLogClient { // verify the user has the required permissions to view this saved objects await this.savedObjectGetter(type, ids); - return await this.esContext.esAdapter.queryEventsBySavedObjects( - this.esContext.esNames.indexPattern, + return await this.esContext.esAdapter.queryEventsBySavedObjects({ + index: this.esContext.esNames.indexPattern, namespace, type, ids, - findOptions - ); + findOptions, + legacyIds, + }); } } diff --git a/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts b/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts index 5259191a98f2b..4685306e869da 100644 --- a/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts +++ b/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts @@ -41,7 +41,7 @@ describe('find_by_ids', () => { eventLogClient, { params: { type: 'action' }, - body: { ids: ['1'] }, + body: { ids: ['1'], legacyIds: ['2'] }, }, ['ok'] ); @@ -50,9 +50,10 @@ describe('find_by_ids', () => { expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - const [type, ids] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; + const [type, ids, , legacyIds] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; expect(type).toEqual(`action`); expect(ids).toEqual(['1']); + expect(legacyIds).toEqual(['2']); expect(res.ok).toHaveBeenCalledWith({ body: result, diff --git a/x-pack/plugins/event_log/server/routes/find_by_ids.ts b/x-pack/plugins/event_log/server/routes/find_by_ids.ts index 130887e5d30a7..378b9516631ad 100644 --- a/x-pack/plugins/event_log/server/routes/find_by_ids.ts +++ b/x-pack/plugins/event_log/server/routes/find_by_ids.ts @@ -23,6 +23,7 @@ const paramSchema = schema.object({ const bodySchema = schema.object({ ids: schema.arrayOf(schema.string(), { defaultValue: [] }), + legacyIds: schema.arrayOf(schema.string(), { defaultValue: [] }), }); export const findByIdsRoute = (router: EventLogRouter, systemLogger: Logger) => { @@ -46,13 +47,13 @@ export const findByIdsRoute = (router: EventLogRouter, systemLogger: Logger) => const eventLogClient = context.eventLog.getEventLogClient(); const { params: { type }, - body: { ids }, + body: { ids, legacyIds }, query, } = req; try { return res.ok({ - body: await eventLogClient.findEventsBySavedObjectIds(type, ids, query), + body: await eventLogClient.findEventsBySavedObjectIds(type, ids, query, legacyIds), }); } catch (err) { const call = `findEventsBySavedObjectIds(${type}, [${ids}], ${JSON.stringify(query)})`; diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 0e5e62b591290..0750e89473b8e 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -45,7 +45,8 @@ export interface IEventLogClient { findEventsBySavedObjectIds( type: string, ids: string[], - options?: Partial + options?: Partial, + legacyIds?: string[] ): Promise; } diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index dde6459addcbc..d3bc4afae6229 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -23,6 +23,8 @@ import type { } from '../../../../common'; import { pkgToPkgKey } from '../registry'; +import { appContextService } from '../../app_context'; + import { getArchiveEntry, setArchiveEntry, setArchiveFilelist, setPackageInfo } from './index'; import type { ArchiveEntry } from './index'; import { parseAndVerifyPolicyTemplates, parseAndVerifyStreams } from './validation'; @@ -165,6 +167,7 @@ export const getEsPackage = async ( references: PackageAssetReference[], savedObjectsClient: SavedObjectsClientContract ) => { + const logger = appContextService.getLogger(); const pkgKey = pkgToPkgKey({ name: pkgName, version: pkgVersion }); const bulkRes = await savedObjectsClient.bulkGet( references.map((reference) => ({ @@ -172,8 +175,27 @@ export const getEsPackage = async ( fields: ['asset_path', 'data_utf8', 'data_base64'], })) ); + const errors = bulkRes.saved_objects.filter((so) => so.error || !so.attributes); const assets = bulkRes.saved_objects.map((so) => so.attributes); + if (errors.length) { + const resolvedErrors = errors.map((so) => + so.error + ? { type: so.type, id: so.id, error: so.error } + : !so.attributes + ? { type: so.type, id: so.id, error: { error: `No attributes retrieved` } } + : { type: so.type, id: so.id, error: { error: `Unknown` } } + ); + + logger.warn( + `Failed to retrieve ${pkgName}-${pkgVersion} package from ES storage. bulkGet failed for assets: ${JSON.stringify( + resolvedErrors + )}` + ); + + return undefined; + } + const paths: string[] = []; const entries: ArchiveEntry[] = assets.map(packageAssetToArchiveEntry); entries.forEach(({ path, buffer }) => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index e493095bc4b36..0e23981b95fcd 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -217,7 +217,10 @@ export async function getPackageFromSource(options: { installedPkg.package_assets, savedObjectsClient ); - logger.debug(`retrieved installed package ${pkgName}-${pkgVersion} from ES`); + + if (res) { + logger.debug(`retrieved installed package ${pkgName}-${pkgVersion} from ES`); + } } // for packages not in cache or package storage and installed from registry, check registry if (!res && pkgInstallSource === 'registry') { diff --git a/x-pack/plugins/global_search/public/services/search_service.test.ts b/x-pack/plugins/global_search/public/services/search_service.test.ts index 4b3c06f03dcc8..b0e6a72290438 100644 --- a/x-pack/plugins/global_search/public/services/search_service.test.ts +++ b/x-pack/plugins/global_search/public/services/search_service.test.ts @@ -272,6 +272,43 @@ describe('SearchService', () => { }); }); + it('catches errors from providers', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + registerResultProvider( + createProvider('A', { + source: hot('a---c-|', { + a: [providerResult('A1'), providerResult('A2')], + c: [providerResult('A3')], + }), + }) + ); + registerResultProvider( + createProvider('B', { + source: hot( + '-b-# ', + { + b: [providerResult('B1')], + }, + new Error('something went bad') + ), + }) + ); + + const { find } = service.start(startDeps()); + const results = find({ term: 'foobar' }, {}); + + expectObservable(results).toBe('ab--c-|', { + a: expectedBatch('A1', 'A2'), + b: expectedBatch('B1'), + c: expectedBatch('A3'), + }); + }); + }); + it('return mixed server/client providers results', async () => { const { registerResultProvider } = service.setup({ config: createConfig(), @@ -304,6 +341,33 @@ describe('SearchService', () => { }); }); + it('catches errors from the server', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + fetchServerResultsMock.mockReturnValue(hot('#', {}, new Error('fetch error'))); + + registerResultProvider( + createProvider('A', { + source: hot('a-b-|', { + a: [providerResult('P1')], + b: [providerResult('P2')], + }), + }) + ); + + const { find } = service.start(startDeps()); + const results = find({ term: 'foobar' }, {}); + + expectObservable(results).toBe('a-b-|', { + a: expectedBatch('P1'), + b: expectedBatch('P2'), + }); + }); + }); + it('handles the `aborted$` option', async () => { const { registerResultProvider } = service.setup({ config: createConfig(), diff --git a/x-pack/plugins/global_search/public/services/search_service.ts b/x-pack/plugins/global_search/public/services/search_service.ts index bf06aa04061ed..85f4d4143a609 100644 --- a/x-pack/plugins/global_search/public/services/search_service.ts +++ b/x-pack/plugins/global_search/public/services/search_service.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { merge, Observable, timer, throwError } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { merge, Observable, timer, throwError, EMPTY } from 'rxjs'; +import { map, takeUntil, catchError } from 'rxjs/operators'; import { uniq } from 'lodash'; import { duration } from 'moment'; import { i18n } from '@kbn/i18n'; @@ -177,16 +177,16 @@ export class SearchService { const serverResults$ = fetchServerResults(this.http!, params, { preference, aborted$, - }); + }).pipe(catchError(() => EMPTY)); const providersResults$ = [...this.providers.values()].map((provider) => provider.find(params, providerOptions).pipe( + catchError(() => EMPTY), takeInArray(this.maxProviderResults), takeUntil(aborted$), map((results) => results.map((r) => processResult(r))) ) ); - return merge(...providersResults$, serverResults$).pipe( map((results) => ({ results, diff --git a/x-pack/plugins/global_search/server/services/search_service.test.ts b/x-pack/plugins/global_search/server/services/search_service.test.ts index 246fbd675aba2..45824fde26afe 100644 --- a/x-pack/plugins/global_search/server/services/search_service.test.ts +++ b/x-pack/plugins/global_search/server/services/search_service.test.ts @@ -178,6 +178,44 @@ describe('SearchService', () => { }); }); + it('catches errors from providers', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + registerResultProvider( + createProvider('A', { + source: hot('a---c-|', { + a: [result('A1'), result('A2')], + c: [result('A3')], + }), + }) + ); + registerResultProvider( + createProvider('B', { + source: hot( + '-b-# ', + { + b: [result('B1')], + }, + new Error('something went bad') + ), + }) + ); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const results = find({ term: 'foobar' }, {}, request); + + expectObservable(results).toBe('ab--c-|', { + a: expectedBatch('A1', 'A2'), + b: expectedBatch('B1'), + c: expectedBatch('A3'), + }); + }); + }); + it('handles the `aborted$` option', async () => { const { registerResultProvider } = service.setup({ config: createConfig(), diff --git a/x-pack/plugins/global_search/server/services/search_service.ts b/x-pack/plugins/global_search/server/services/search_service.ts index a6c2a7ee234d6..22bac036544ab 100644 --- a/x-pack/plugins/global_search/server/services/search_service.ts +++ b/x-pack/plugins/global_search/server/services/search_service.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { Observable, timer, merge, throwError } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { Observable, timer, merge, throwError, EMPTY } from 'rxjs'; +import { map, takeUntil, catchError } from 'rxjs/operators'; import { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { KibanaRequest, CoreStart, IBasePath } from 'src/core/server'; @@ -174,6 +174,7 @@ export class SearchService { const providersResults$ = [...this.providers.values()].map((provider) => provider.find(params, findOptions, context).pipe( + catchError(() => EMPTY), takeInArray(this.maxProviderResults), takeUntil(aborted$), map((results) => results.map((r) => processResult(r))) diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts index 94d26abc701fe..c2449707647d7 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_api.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts @@ -78,7 +78,9 @@ export const MetricsAPISeriesRT = rt.intersection([ ]); export const MetricsAPIResponseRT = rt.type({ - series: rt.array(MetricsAPISeriesRT), + series: rt.array( + rt.intersection([MetricsAPISeriesRT, rt.partial({ metricsets: rt.array(rt.string) })]) + ), info: MetricsAPIPageInfoRT, }); diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index a2d9ad2b401d4..131f6944b0484 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -91,12 +91,14 @@ export const query = async ( return { series: groupings.buckets.map((bucket) => { const keys = Object.values(bucket.key); - return convertHistogramBucketsToTimeseries( + const metricsetNames = bucket.metricsets.buckets.map((m) => m.key); + const timeseries = convertHistogramBucketsToTimeseries( keys, options, bucket.histogram.buckets, bucketSize * 1000 ); + return { ...timeseries, metricsets: metricsetNames }; }), info: { afterKey: returnAfterKey ? afterKey : null, diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap index c7acfb147aa7e..d35d7ce1a4b81 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap +++ b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap @@ -20,6 +20,11 @@ Object { "offset": "-60000ms", }, }, + "metricsets": Object { + "terms": Object { + "field": "metricset.name", + }, + }, } `; @@ -45,6 +50,11 @@ Object { "offset": "0s", }, }, + "metricsets": Object { + "terms": Object { + "field": "metricset.name", + }, + }, }, "composite": Object { "size": 20, @@ -82,5 +92,10 @@ Object { "offset": "0s", }, }, + "metricsets": Object { + "terms": Object { + "field": "metricset.name", + }, + }, } `; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts index 1fb87499b8f44..65cd4ebe2d501 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts @@ -25,6 +25,11 @@ export const createAggregations = (options: MetricsAPIRequest) => { }, aggregations: createMetricsAggregations(options), }, + metricsets: { + terms: { + field: 'metricset.name', + }, + }, }; if (Array.isArray(options.groupBy) && options.groupBy.length) { diff --git a/x-pack/plugins/infra/server/lib/metrics/types.ts b/x-pack/plugins/infra/server/lib/metrics/types.ts index e1f5ecd45e363..b18486f88cc4c 100644 --- a/x-pack/plugins/infra/server/lib/metrics/types.ts +++ b/x-pack/plugins/infra/server/lib/metrics/types.ts @@ -63,6 +63,14 @@ export const HistogramResponseRT = rt.type({ histogram: rt.type({ buckets: rt.array(HistogramBucketRT), }), + metricsets: rt.type({ + buckets: rt.array( + rt.type({ + key: rt.string, + doc_count: rt.number, + }) + ), + }), }); const GroupingBucketRT = rt.intersection([ diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index 0fef75faed07e..f59756e0c5b25 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -10,7 +10,7 @@ import { ESSearchClient } from '../../../lib/metrics/types'; import { InfraSource } from '../../../lib/sources'; import { transformRequestToMetricsAPIRequest } from './transform_request_to_metrics_api_request'; import { queryAllData } from './query_all_data'; -import { transformMetricsApiResponseToSnapshotResponse } from './trasform_metrics_ui_response'; +import { transformMetricsApiResponseToSnapshotResponse } from './transform_metrics_ui_response'; import { copyMissingMetrics } from './copy_missing_metrics'; import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_metrics_ui_response.test.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_metrics_ui_response.test.ts new file mode 100644 index 0000000000000..159f6d0b646ec --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_metrics_ui_response.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { transformMetricsApiResponseToSnapshotResponse } from './transform_metrics_ui_response'; + +jest.mock('./apply_metadata_to_last_path', () => ({ + applyMetadataToLastPath: (series: any) => [{ label: series.id }], +})); + +const now = 1630597319235; + +describe('transformMetricsApiResponseToSnapshotResponse', () => { + test('filters out nodes from APM which report no data', () => { + const result = transformMetricsApiResponseToSnapshotResponse( + { + // @ts-ignore + metrics: [{ id: 'cpu' }], + }, + { + includeTimeseries: false, + nodeType: 'host', + }, + {}, + { + info: { + interval: 60, + }, + series: [ + { + metricsets: ['app'], + id: 'apm-node-with-no-data', + columns: [], + rows: [ + { + timestamp: now, + cpu: null, + }, + ], + }, + { + metricsets: ['app'], + id: 'apm-node-with-data', + columns: [], + rows: [ + { + timestamp: now, + cpu: 1.0, + }, + ], + }, + { + metricsets: ['cpu'], + id: 'metricbeat-node', + columns: [], + rows: [ + { + timestamp: now, + cpu: 1.0, + }, + ], + }, + ], + } + ); + const nodeNames = result.nodes.map((n) => n.name); + expect(nodeNames).toEqual(expect.arrayContaining(['metricbeat-node', 'apm-node-with-data'])); + expect(nodeNames).not.toEqual(expect.arrayContaining(['apm-node'])); + }); +}); diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_metrics_ui_response.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_metrics_ui_response.ts new file mode 100644 index 0000000000000..a76093f78e08c --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_metrics_ui_response.ts @@ -0,0 +1,96 @@ +/* + * 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 { get, max, sum, last, isNumber } from 'lodash'; +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { + MetricsAPIResponse, + SnapshotNodeResponse, + MetricsAPIRequest, + MetricsExplorerColumnType, + MetricsAPIRow, + SnapshotRequest, + SnapshotNodePath, + SnapshotNodeMetric, + SnapshotNode, +} from '../../../../common/http_api'; +import { META_KEY } from './constants'; +import { InfraSource } from '../../../lib/sources'; +import { applyMetadataToLastPath } from './apply_metadata_to_last_path'; + +const getMetricValue = (row: MetricsAPIRow) => { + if (!isNumber(row.metric_0)) return null; + const value = row.metric_0; + return isFinite(value) ? value : null; +}; + +const calculateMax = (rows: MetricsAPIRow[]) => { + return max(rows.map(getMetricValue)) || 0; +}; + +const calculateAvg = (rows: MetricsAPIRow[]): number => { + return sum(rows.map(getMetricValue)) / rows.length || 0; +}; + +const getLastValue = (rows: MetricsAPIRow[]) => { + const row = last(rows); + if (!row) return null; + return getMetricValue(row); +}; + +export const transformMetricsApiResponseToSnapshotResponse = ( + options: MetricsAPIRequest, + snapshotRequest: SnapshotRequest, + source: InfraSource, + metricsApiResponse: MetricsAPIResponse +): SnapshotNodeResponse => { + const nodes = metricsApiResponse.series + .map((series) => { + const node = { + metrics: options.metrics + .filter((m) => m.id !== META_KEY) + .map((metric) => { + const name = metric.id as SnapshotMetricType; + const timeseries = { + id: name, + columns: [ + { name: 'timestamp', type: 'date' as MetricsExplorerColumnType }, + { name: 'metric_0', type: 'number' as MetricsExplorerColumnType }, + ], + rows: series.rows.map((row) => { + return { timestamp: row.timestamp, metric_0: get(row, metric.id, null) }; + }), + }; + const maxValue = calculateMax(timeseries.rows); + const avg = calculateAvg(timeseries.rows); + const value = getLastValue(timeseries.rows); + const nodeMetric: SnapshotNodeMetric = { name, max: maxValue, value, avg }; + if (snapshotRequest.includeTimeseries) { + nodeMetric.timeseries = timeseries; + } + return nodeMetric; + }), + path: + series.keys?.map((key) => { + return { value: key, label: key } as SnapshotNodePath; + }) ?? [], + name: '', + }; + + const isNoData = node.metrics.every((m) => m.value === null); + const isAPMNode = series.metricsets?.includes('app'); + if (isNoData && isAPMNode) return null; + + const path = applyMetadataToLastPath(series, node, snapshotRequest, source); + const lastPath = last(path); + const name = lastPath?.label ?? 'N/A'; + + return { ...node, path, name }; + }) + .filter((n) => n !== null) as SnapshotNode[]; + return { nodes, interval: `${metricsApiResponse.info.interval}s` }; +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts deleted file mode 100644 index 21b3e07e83588..0000000000000 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts +++ /dev/null @@ -1,88 +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 { get, max, sum, last, isNumber } from 'lodash'; -import { SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { - MetricsAPIResponse, - SnapshotNodeResponse, - MetricsAPIRequest, - MetricsExplorerColumnType, - MetricsAPIRow, - SnapshotRequest, - SnapshotNodePath, - SnapshotNodeMetric, -} from '../../../../common/http_api'; -import { META_KEY } from './constants'; -import { InfraSource } from '../../../lib/sources'; -import { applyMetadataToLastPath } from './apply_metadata_to_last_path'; - -const getMetricValue = (row: MetricsAPIRow) => { - if (!isNumber(row.metric_0)) return null; - const value = row.metric_0; - return isFinite(value) ? value : null; -}; - -const calculateMax = (rows: MetricsAPIRow[]) => { - return max(rows.map(getMetricValue)) || 0; -}; - -const calculateAvg = (rows: MetricsAPIRow[]): number => { - return sum(rows.map(getMetricValue)) / rows.length || 0; -}; - -const getLastValue = (rows: MetricsAPIRow[]) => { - const row = last(rows); - if (!row) return null; - return getMetricValue(row); -}; - -export const transformMetricsApiResponseToSnapshotResponse = ( - options: MetricsAPIRequest, - snapshotRequest: SnapshotRequest, - source: InfraSource, - metricsApiResponse: MetricsAPIResponse -): SnapshotNodeResponse => { - const nodes = metricsApiResponse.series.map((series) => { - const node = { - metrics: options.metrics - .filter((m) => m.id !== META_KEY) - .map((metric) => { - const name = metric.id as SnapshotMetricType; - const timeseries = { - id: name, - columns: [ - { name: 'timestamp', type: 'date' as MetricsExplorerColumnType }, - { name: 'metric_0', type: 'number' as MetricsExplorerColumnType }, - ], - rows: series.rows.map((row) => { - return { timestamp: row.timestamp, metric_0: get(row, metric.id, null) }; - }), - }; - const maxValue = calculateMax(timeseries.rows); - const avg = calculateAvg(timeseries.rows); - const value = getLastValue(timeseries.rows); - const nodeMetric: SnapshotNodeMetric = { name, max: maxValue, value, avg }; - if (snapshotRequest.includeTimeseries) { - nodeMetric.timeseries = timeseries; - } - return nodeMetric; - }), - path: - series.keys?.map((key) => { - return { value: key, label: key } as SnapshotNodePath; - }) ?? [], - name: '', - }; - - const path = applyMetadataToLastPath(series, node, snapshotRequest, source); - const lastPath = last(path); - const name = (lastPath && lastPath.label) || 'N/A'; - return { ...node, path, name }; - }); - return { nodes, interval: `${metricsApiResponse.info.interval}s` }; -}; diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 2ec7a1962da82..f82f3366448da 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -24,7 +24,8 @@ "usageCollection", "taskManager", "globalSearch", - "savedObjectsTagging" + "savedObjectsTagging", + "spaces" ], "configPath": [ "xpack", diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 8cb4a7c4c8433..5617b5b0edeea 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -383,6 +383,9 @@ describe('Lens App', () => { savedObjectId: savedObjectId || 'aaa', })); services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, savedObjectId: initialSavedObjectId ?? 'aaa', references: [], state: { @@ -1256,4 +1259,32 @@ describe('Lens App', () => { expect(defaultLeave).not.toHaveBeenCalled(); }); }); + it('should display a conflict callout if saved object conflicts', async () => { + const history = createMemoryHistory(); + const { services } = await mountWith({ + props: { + ...makeDefaultProps(), + history: { + ...history, + location: { + ...history.location, + search: '?_g=test', + }, + }, + }, + preloadedState: { + persistedDoc: defaultDoc, + sharingSavedObjectProps: { + outcome: 'conflict', + aliasTargetId: '2', + }, + }, + }); + expect(services.spaces.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + currentObjectId: '1234', + objectNoun: 'Lens visualization', + otherObjectId: '2', + otherObjectPath: '#/edit/2?_g=test', + }); + }); }); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 63cb7d3002542..ae2edaa1b98d3 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -38,6 +38,7 @@ import { runSaveLensVisualization, } from './save_modal_container'; import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; +import { getEditPath } from '../../common'; export type SaveProps = Omit & { returnToOrigin: boolean; @@ -70,6 +71,8 @@ export function App({ notifications, savedObjectsTagging, getOriginatingAppName, + spaces, + http, // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag, } = lensAppServices; @@ -82,6 +85,7 @@ export function App({ const { persistedDoc, + sharingSavedObjectProps, isLinkedToOriginatingApp, searchSessionId, isLoading, @@ -166,6 +170,28 @@ export function App({ }); }, [onAppLeave, lastKnownDoc, isSaveable, persistedDoc, application.capabilities.visualize.save]); + const getLegacyUrlConflictCallout = useCallback(() => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + if (spaces && sharingSavedObjectProps?.outcome === 'conflict' && persistedDoc?.savedObjectId) { + // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other object. + const currentObjectId = persistedDoc.savedObjectId; + const otherObjectId = sharingSavedObjectProps?.aliasTargetId!; // This is always defined if outcome === 'conflict' + const otherObjectPath = http.basePath.prepend( + `${getEditPath(otherObjectId)}${history.location.search}` + ); + return spaces.ui.components.getLegacyUrlConflict({ + objectNoun: i18n.translate('xpack.lens.appName', { + defaultMessage: 'Lens visualization', + }), + currentObjectId, + otherObjectId, + otherObjectPath, + }); + } + return null; + }, [persistedDoc, sharingSavedObjectProps, spaces, http, history]); + // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { const isByValueMode = getIsByValueMode(); @@ -273,6 +299,8 @@ export function App({ title={persistedDoc?.title} lensInspector={lensInspector} /> + + {getLegacyUrlConflictCallout()} {(!isLoading || persistedDoc) && ( diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 4ccf441799b1c..8a3a848ffa204 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -7,6 +7,7 @@ import type { History } from 'history'; import type { OnSaveProps } from 'src/plugins/saved_objects/public'; +import { SpacesApi } from '../../../spaces/public'; import type { ApplicationStart, AppMountParameters, @@ -116,6 +117,8 @@ export interface LensAppServices { savedObjectsTagging?: SavedObjectTaggingPluginStart; getOriginatingAppName: () => string | undefined; presentationUtil: PresentationUtilPluginStart; + spaces: SpacesApi; + // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag: DashboardFeatureFlagConfig; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 31705d6b92933..c34e3c4137368 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -79,7 +79,7 @@ export interface WorkspacePanelProps { interface WorkspaceState { expressionBuildError?: Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; fixAction?: DatasourceFixAction; }>; expandError: boolean; @@ -416,10 +416,10 @@ export const VisualizationWrapper = ({ localState: WorkspaceState & { configurationValidationError?: Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; fixAction?: DatasourceFixAction; }>; - missingRefsErrors?: Array<{ shortMessage: string; longMessage: string }>; + missingRefsErrors?: Array<{ shortMessage: string; longMessage: React.ReactNode }>; }; ExpressionRendererComponent: ReactExpressionRendererType; application: ApplicationStart; @@ -454,7 +454,7 @@ export const VisualizationWrapper = ({ validationError: | { shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; fixAction?: DatasourceFixAction; } | undefined @@ -499,7 +499,7 @@ export const VisualizationWrapper = ({ .map((validationError) => ( <>

diff --git a/x-pack/plugins/lens/public/editor_frame_service/types.ts b/x-pack/plugins/lens/public/editor_frame_service/types.ts index ebfd098b5fb19..9435faf374420 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/types.ts @@ -11,6 +11,6 @@ export type TableInspectorAdapter = Record; export interface ErrorMessage { shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; type?: 'fixable' | 'critical'; } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 74aac932a6861..a0831e8a73b57 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -11,6 +11,7 @@ import { LensByReferenceInput, LensSavedObjectAttributes, LensEmbeddableInput, + ResolvedLensSavedObjectAttributes, } from './embeddable'; import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; import { Query, TimeRange, Filter, IndexPatternsContract } from 'src/plugins/data/public'; @@ -68,12 +69,17 @@ const options = { const attributeServiceMockFromSavedVis = (document: Document): LensAttributeService => { const core = coreMock.createStart(); const service = new AttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >('lens', jest.fn(), core.i18n.Context, core.notifications.toasts, options); service.unwrapAttributes = jest.fn((input: LensByValueInput | LensByReferenceInput) => { - return Promise.resolve({ ...document } as LensSavedObjectAttributes); + return Promise.resolve({ + ...document, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + } as ResolvedLensSavedObjectAttributes); }); service.wrapAttributes = jest.fn(); return service; @@ -86,7 +92,7 @@ describe('embeddable', () => { let trigger: { exec: jest.Mock }; let basePath: IBasePath; let attributeService: AttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >; @@ -223,6 +229,50 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(0); }); + it('should not render the vis if loaded saved object conflicts', async () => { + attributeService.unwrapAttributes = jest.fn( + (input: LensByValueInput | LensByReferenceInput) => { + return Promise.resolve({ + ...savedVis, + sharingSavedObjectProps: { + outcome: 'conflict', + errorJSON: '{targetType: "lens", sourceId: "1", targetSpace: "space"}', + aliasTargetId: '2', + }, + } as ResolvedLensSavedObjectAttributes); + } + ); + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + inspector: inspectorPluginMock.createStartContract(), + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, + getTrigger, + documentToExpression: () => + Promise.resolve({ + ast: { + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }, + errors: undefined, + }), + }, + {} as LensEmbeddableInput + ); + await embeddable.initializeSavedVis({} as LensEmbeddableInput); + expect(expressionRenderer).toHaveBeenCalledTimes(0); + }); + it('should initialize output with deduped list of index patterns', async () => { attributeService = attributeServiceMockFromSavedVis({ ...savedVis, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 172274b1f90bc..7e87dd3076faa 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -41,7 +41,11 @@ import { ReferenceOrValueEmbeddable, } from '../../../../../src/plugins/embeddable/public'; import { Document, injectFilterReferences } from '../persistence'; -import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper'; +import { + ExpressionWrapper, + ExpressionWrapperProps, + savedObjectConflictError, +} from './expression_wrapper'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { isLensBrushEvent, @@ -58,8 +62,12 @@ import { IBasePath } from '../../../../../src/core/public'; import { LensAttributeService } from '../lens_attribute_service'; import type { ErrorMessage } from '../editor_frame_service/types'; import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; +import { SharingSavedObjectProps } from '../types'; export type LensSavedObjectAttributes = Omit; +export interface ResolvedLensSavedObjectAttributes extends LensSavedObjectAttributes { + sharingSavedObjectProps?: SharingSavedObjectProps; +} interface LensBaseEmbeddableInput extends EmbeddableInput { filters?: Filter[]; @@ -76,7 +84,7 @@ interface LensBaseEmbeddableInput extends EmbeddableInput { } export type LensByValueInput = { - attributes: LensSavedObjectAttributes; + attributes: ResolvedLensSavedObjectAttributes; } & LensBaseEmbeddableInput; export type LensByReferenceInput = SavedObjectEmbeddableInput & LensBaseEmbeddableInput; @@ -253,15 +261,18 @@ export class Embeddable } async initializeSavedVis(input: LensEmbeddableInput) { - const attributes: - | LensSavedObjectAttributes + const attrs: + | ResolvedLensSavedObjectAttributes | false = await this.deps.attributeService.unwrapAttributes(input).catch((e: Error) => { this.onFatalError(e); return false; }); - if (!attributes || this.isDestroyed) { + if (!attrs || this.isDestroyed) { return; } + + const { sharingSavedObjectProps, ...attributes } = attrs; + this.savedVis = { ...attributes, type: this.type, @@ -269,8 +280,12 @@ export class Embeddable }; const { ast, errors } = await this.deps.documentToExpression(this.savedVis); this.errors = errors; + if (sharingSavedObjectProps?.outcome === 'conflict') { + const conflictError = savedObjectConflictError(sharingSavedObjectProps.errorJSON!); + this.errors = this.errors ? [...this.errors, conflictError] : [conflictError]; + } this.expression = ast ? toExpression(ast) : null; - if (errors) { + if (this.errors) { this.logError('validation'); } await this.initializeOutput(); diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index d57e1c450fea2..1116b4a0d3963 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -5,10 +5,20 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon, EuiEmptyPrompt } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiIcon, + EuiEmptyPrompt, + EuiButtonEmpty, + EuiCallOut, + EuiSpacer, + EuiLink, +} from '@elastic/eui'; import { ExpressionRendererEvent, ReactExpressionRendererType, @@ -18,6 +28,7 @@ import type { KibanaExecutionContext } from 'src/core/public'; import { ExecutionContextSearch } from 'src/plugins/data/public'; import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions'; import classNames from 'classnames'; +import { i18n } from '@kbn/i18n'; import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper'; import { ErrorMessage } from '../editor_frame_service/types'; import { LensInspector } from '../lens_inspector_service'; @@ -158,3 +169,52 @@ export function ExpressionWrapper({ ); } + +const SavedObjectConflictMessage = ({ json }: { json: string }) => { + const [expandError, setExpandError] = useState(false); + return ( + <> + + {i18n.translate('xpack.lens.embeddable.legacyURLConflict.documentationLinkText', { + defaultMessage: 'legacy URL alias', + })} + + ), + }} + /> + + {expandError ? ( + + ) : ( + setExpandError(true)}> + {i18n.translate('xpack.lens.embeddable.legacyURLConflict.expandError', { + defaultMessage: `Show more`, + })} + + )} + + ); +}; + +export const savedObjectConflictError = (json: string): ErrorMessage => ({ + shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', { + defaultMessage: `You've encountered a URL conflict`, + }), + longMessage: , +}); diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index 39a1903c6d0c4..09c98b3dcba72 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -9,47 +9,68 @@ import { CoreStart } from '../../../../src/core/public'; import { LensPluginStartDependencies } from './plugin'; import { AttributeService } from '../../../../src/plugins/embeddable/public'; import { - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput, } from './embeddable/embeddable'; -import { SavedObjectIndexStore, Document } from './persistence'; +import { SavedObjectIndexStore } from './persistence'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { DOC_TYPE } from '../common'; export type LensAttributeService = AttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >; -function documentToAttributes(doc: Document): LensSavedObjectAttributes { - delete doc.savedObjectId; - delete doc.type; - return { ...doc }; -} - export function getLensAttributeService( core: CoreStart, startDependencies: LensPluginStartDependencies ): LensAttributeService { const savedObjectStore = new SavedObjectIndexStore(core.savedObjects.client); return startDependencies.embeddable.getAttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >(DOC_TYPE, { - saveMethod: async (attributes: LensSavedObjectAttributes, savedObjectId?: string) => { + saveMethod: async (attributes: ResolvedLensSavedObjectAttributes, savedObjectId?: string) => { + const { sharingSavedObjectProps, ...attributesToSave } = attributes; const savedDoc = await savedObjectStore.save({ - ...attributes, + ...attributesToSave, savedObjectId, type: DOC_TYPE, }); return { id: savedDoc.savedObjectId }; }, - unwrapMethod: async (savedObjectId: string): Promise => { - const attributes = documentToAttributes(await savedObjectStore.load(savedObjectId)); - return attributes; + unwrapMethod: async (savedObjectId: string): Promise => { + const { + saved_object: savedObject, + outcome, + alias_target_id: aliasTargetId, + } = await savedObjectStore.load(savedObjectId); + const { attributes, references, type, id } = savedObject; + const document = { + ...attributes, + references, + }; + + const sharingSavedObjectProps = { + aliasTargetId, + outcome, + errorJSON: + outcome === 'conflict' + ? JSON.stringify({ + targetType: type, + sourceId: id, + targetSpace: (await startDependencies.spaces.getActiveSpace()).id, + }) + : undefined, + }; + + return { + sharingSavedObjectProps, + ...document, + }; }, checkForDuplicateTitle: (props: OnSaveProps) => { const savedObjectsClient = core.savedObjects.client; diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index b2c8d3948b285..8fbd263fe909e 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -24,11 +24,12 @@ import { LensAppServices } from './app_plugin/types'; import { DOC_TYPE, layerTypes } from '../common'; import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public'; import { inspectorPluginMock } from '../../../../src/plugins/inspector/public/mocks'; +import { spacesPluginMock } from '../../spaces/public/mocks'; import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks'; import type { LensByValueInput, - LensSavedObjectAttributes, LensByReferenceInput, + ResolvedLensSavedObjectAttributes, } from './embeddable/embeddable'; import { mockAttributeService, @@ -352,7 +353,7 @@ export function makeDefaultServices( function makeAttributeService(): LensAttributeService { const attributeServiceMock = mockAttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >( @@ -365,7 +366,12 @@ export function makeDefaultServices( core ); - attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(doc); + attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue({ + ...doc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }); attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ savedObjectId: ((doc as unknown) as LensByReferenceInput).savedObjectId, }); @@ -404,6 +410,7 @@ export function makeDefaultServices( remove: jest.fn(), clear: jest.fn(), }, + spaces: spacesPluginMock.createStartContract(), }; } diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts index ab0708d99f082..5a42ea054b4d9 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts @@ -15,7 +15,7 @@ describe('LensStore', () => { bulkUpdate: jest.fn(([{ id }]: SavedObjectsBulkUpdateObject[]) => Promise.resolve({ savedObjects: [{ id }, { id }] }) ), - get: jest.fn(), + resolve: jest.fn(), }; return { @@ -142,15 +142,18 @@ describe('LensStore', () => { describe('load', () => { test('throws if an error is returned', async () => { const { client, store } = testStore(); - client.get = jest.fn(async () => ({ - id: 'Paul', - type: 'lens', - attributes: { - title: 'Hope clouds observation.', - visualizationType: 'dune', - state: '{ "datasource": { "giantWorms": true } }', + client.resolve = jest.fn(async () => ({ + outcome: 'exactMatch', + saved_object: { + id: 'Paul', + type: 'lens', + attributes: { + title: 'Hope clouds observation.', + visualizationType: 'dune', + state: '{ "datasource": { "giantWorms": true } }', + }, + error: new Error('shoot dang!'), }, - error: new Error('shoot dang!'), })); await expect(store.load('Paul')).rejects.toThrow('shoot dang!'); diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index c87548daf53dc..79d7b78f768ae 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -9,9 +9,11 @@ import { SavedObjectAttributes, SavedObjectsClientContract, SavedObjectReference, + ResolvedSimpleSavedObject, } from 'kibana/public'; import { Query } from '../../../../../src/plugins/data/public'; import { DOC_TYPE, PersistableFilter } from '../../common'; +import { LensSavedObjectAttributes } from '../async_services'; export interface Document { savedObjectId?: string; @@ -37,7 +39,7 @@ export interface DocumentSaver { } export interface DocumentLoader { - load: (savedObjectId: string) => Promise; + load: (savedObjectId: string) => Promise; } export type SavedObjectStore = DocumentLoader & DocumentSaver; @@ -87,18 +89,16 @@ export class SavedObjectIndexStore implements SavedObjectStore { ).savedObjects[1]; } - async load(savedObjectId: string): Promise { - const { type, attributes, references, error } = await this.client.get(DOC_TYPE, savedObjectId); + async load(savedObjectId: string): Promise> { + const resolveResult = await this.client.resolve( + DOC_TYPE, + savedObjectId + ); - if (error) { - throw error; + if (resolveResult.saved_object.error) { + throw resolveResult.saved_object.error; } - return { - ...(attributes as SavedObjectAttributes), - references, - savedObjectId, - type, - } as Document; + return resolveResult; } } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 95f2e13cbc464..26278f446c558 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -9,6 +9,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import type { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public'; import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public'; +import { SpacesPluginStart } from '../../spaces/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { DashboardStart } from '../../../../src/plugins/dashboard/public'; @@ -100,6 +101,7 @@ export interface LensPluginStartDependencies { presentationUtil: PresentationUtilPluginStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; inspector: InspectorStartContract; + spaces: SpacesPluginStart; usageCollection?: UsageCollectionStart; } diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx index ad1755bdbe85c..cda891871168e 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiColorPalettePickerPaletteProps } from '@elastic/eui'; +import { EuiButtonGroup, EuiColorPalettePickerPaletteProps } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test/jest'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; @@ -14,6 +14,7 @@ import { ReactWrapper } from 'enzyme'; import type { CustomPaletteParams } from '../../../common'; import { applyPaletteParams } from './utils'; import { CustomizablePalette } from './palette_configuration'; +import { act } from 'react-dom/test-utils'; // mocking random id generator function jest.mock('@elastic/eui', () => { @@ -128,71 +129,136 @@ describe('palette panel', () => { }); }); - describe('reverse option', () => { - beforeEach(() => { - props = { - activePalette: { type: 'palette', name: 'positive' }, - palettes: paletteRegistry, - setPalette: jest.fn(), - dataBounds: { min: 0, max: 100 }, - }; - }); + it('should rewrite the min/max range values on palette change', () => { + const instance = mountWithIntl(); + + changePaletteIn(instance, 'custom'); - function toggleReverse(instance: ReactWrapper, checked: boolean) { - return instance - .find('[data-test-subj="lnsPalettePanel_dynamicColoring_reverse"]') - .first() - .prop('onClick')!({} as React.MouseEvent); - } - - it('should reverse the colorStops on click', () => { - const instance = mountWithIntl(); - - toggleReverse(instance, true); - - expect(props.setPalette).toHaveBeenCalledWith( - expect.objectContaining({ - params: expect.objectContaining({ - reverse: true, - }), - }) - ); + expect(props.setPalette).toHaveBeenCalledWith({ + type: 'palette', + name: 'custom', + params: expect.objectContaining({ + rangeMin: 0, + rangeMax: 50, + }), }); }); + }); + + describe('reverse option', () => { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 0, max: 100 }, + }; + }); + + function toggleReverse(instance: ReactWrapper, checked: boolean) { + return instance + .find('[data-test-subj="lnsPalettePanel_dynamicColoring_reverse"]') + .first() + .prop('onClick')!({} as React.MouseEvent); + } + + it('should reverse the colorStops on click', () => { + const instance = mountWithIntl(); + + toggleReverse(instance, true); + + expect(props.setPalette).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + reverse: true, + }), + }) + ); + }); + }); + + describe('percentage / number modes', () => { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 5, max: 200 }, + }; + }); - describe('custom stops', () => { - beforeEach(() => { - props = { - activePalette: { type: 'palette', name: 'positive' }, - palettes: paletteRegistry, - setPalette: jest.fn(), - dataBounds: { min: 0, max: 100 }, - }; + it('should switch mode and range boundaries on click', () => { + const instance = mountWithIntl(); + act(() => { + instance + .find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"]') + .find(EuiButtonGroup) + .prop('onChange')!('number'); }); - it('should be visible for predefined palettes', () => { - const instance = mountWithIntl(); - expect( - instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists() - ).toEqual(true); + + act(() => { + instance + .find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"]') + .find(EuiButtonGroup) + .prop('onChange')!('percent'); }); - it('should be visible for custom palettes', () => { - const instance = mountWithIntl( - { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 0, max: 100 }, + }; + }); + it('should be visible for predefined palettes', () => { + const instance = mountWithIntl(); + expect( + instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists() + ).toEqual(true); + }); + + it('should be visible for custom palettes', () => { + const instance = mountWithIntl( + - ); - expect( - instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists() - ).toEqual(true); - }); + }, + }} + /> + ); + expect( + instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists() + ).toEqual(true); }); }); }); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx index bc6a590db0cb7..1d1e212b87c0c 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx @@ -108,16 +108,21 @@ export function CustomizablePalette({ colorStops: undefined, }; + const newColorStops = getColorStops(palettes, [], activePalette, dataBounds); if (isNewPaletteCustom) { - newParams.colorStops = getColorStops(palettes, [], activePalette, dataBounds); + newParams.colorStops = newColorStops; } newParams.stops = getPaletteStops(palettes, newParams, { prevPalette: isNewPaletteCustom || isCurrentPaletteCustom ? undefined : newPalette.name, dataBounds, + mapFromMinValue: true, }); + newParams.rangeMin = newColorStops[0].stop; + newParams.rangeMax = newColorStops[newColorStops.length - 1].stop; + setPalette({ ...newPalette, params: newParams, @@ -266,18 +271,18 @@ export function CustomizablePalette({ ) as RequiredPaletteParamTypes['rangeType']; const params: CustomPaletteParams = { rangeType: newRangeType }; + const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds); + const { min: oldMin, max: oldMax } = getDataMinMax( + activePalette.params?.rangeType, + dataBounds + ); + const newColorStops = remapStopsByNewInterval(colorStopsToShow, { + oldInterval: oldMax - oldMin, + newInterval: newMax - newMin, + newMin, + oldMin, + }); if (isCurrentPaletteCustom) { - const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds); - const { min: oldMin, max: oldMax } = getDataMinMax( - activePalette.params?.rangeType, - dataBounds - ); - const newColorStops = remapStopsByNewInterval(colorStopsToShow, { - oldInterval: oldMax - oldMin, - newInterval: newMax - newMin, - newMin, - oldMin, - }); const stops = getPaletteStops( palettes, { ...activePalette.params, colorStops: newColorStops, ...params }, @@ -285,8 +290,6 @@ export function CustomizablePalette({ ); params.colorStops = newColorStops; params.stops = stops; - params.rangeMin = newColorStops[0].stop; - params.rangeMax = newColorStops[newColorStops.length - 1].stop; } else { params.stops = getPaletteStops( palettes, @@ -294,6 +297,11 @@ export function CustomizablePalette({ { prevPalette: activePalette.name, dataBounds } ); } + // why not use newMin/newMax here? + // That's because there's the concept of continuity to accomodate, where in some scenarios it has to + // take into account the stop value rather than the data value + params.rangeMin = newColorStops[0].stop; + params.rangeMax = newColorStops[newColorStops.length - 1].stop; setPalette(mergePaletteParams(activePalette, params)); }} /> diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts index 97dc2e45c96dc..07d93ca5c40c6 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts @@ -8,6 +8,7 @@ import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { applyPaletteParams, + getColorStops, getContrastColor, getDataMinMax, getPaletteStops, @@ -59,6 +60,78 @@ describe('applyPaletteParams', () => { }); }); +describe('getColorStops', () => { + const paletteRegistry = chartPluginMock.createPaletteRegistry(); + it('should return the same colorStops if a custom palette is passed, avoiding recomputation', () => { + const colorStops = [ + { stop: 0, color: 'red' }, + { stop: 100, color: 'blue' }, + ]; + expect( + getColorStops( + paletteRegistry, + colorStops, + { name: 'custom', type: 'palette' }, + { min: 0, max: 100 } + ) + ).toBe(colorStops); + }); + + it('should get a fresh list of colors', () => { + expect( + getColorStops( + paletteRegistry, + [ + { stop: 0, color: 'red' }, + { stop: 100, color: 'blue' }, + ], + { name: 'mocked', type: 'palette' }, + { min: 0, max: 100 } + ) + ).toEqual([ + { color: 'blue', stop: 0 }, + { color: 'yellow', stop: 50 }, + ]); + }); + + it('should get a fresh list of colors even if custom palette but empty colorStops', () => { + expect( + getColorStops(paletteRegistry, [], { name: 'mocked', type: 'palette' }, { min: 0, max: 100 }) + ).toEqual([ + { color: 'blue', stop: 0 }, + { color: 'yellow', stop: 50 }, + ]); + }); + + it('should correctly map the new colorStop to the current data bound and minValue', () => { + expect( + getColorStops( + paletteRegistry, + [], + { name: 'mocked', type: 'palette', params: { rangeType: 'number' } }, + { min: 100, max: 1000 } + ) + ).toEqual([ + { color: 'blue', stop: 100 }, + { color: 'yellow', stop: 550 }, + ]); + }); + + it('should reverse the colors', () => { + expect( + getColorStops( + paletteRegistry, + [], + { name: 'mocked', type: 'palette', params: { reverse: true } }, + { min: 100, max: 1000 } + ) + ).toEqual([ + { color: 'yellow', stop: 0 }, + { color: 'blue', stop: 50 }, + ]); + }); +}); + describe('remapStopsByNewInterval', () => { it('should correctly remap the current palette from 0..1 to 0...100', () => { expect( diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts index b2969565f5390..413e3708e9c9b 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts @@ -269,11 +269,10 @@ export function getColorStops( palettes: PaletteRegistry, colorStops: Required['stops'], activePalette: PaletteOutput, - dataBounds: { min: number; max: number }, - defaultPalette?: string + dataBounds: { min: number; max: number } ) { // just forward the current stops if custom - if (activePalette?.name === CUSTOM_PALETTE) { + if (activePalette?.name === CUSTOM_PALETTE && colorStops?.length) { return colorStops; } // for predefined palettes create some stops, then drop the last one. diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts index bf13ca69e82c0..256684c5dbc25 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts @@ -19,13 +19,7 @@ export const initMiddleware = (storeDeps: LensStoreDeps) => (store: MiddlewareAP ); return (next: Dispatch) => (action: PayloadAction) => { if (lensSlice.actions.loadInitial.match(action)) { - return loadInitial( - store, - storeDeps, - action.payload.redirectCallback, - action.payload.initialInput, - action.payload.emptyState - ); + return loadInitial(store, storeDeps, action.payload); } else if (lensSlice.actions.navigateAway.match(action)) { return unsubscribeFromExternalContext(); } diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx index 79402b698af98..6d3b77c6476e5 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx @@ -12,6 +12,7 @@ import { createMockDatasource, DatasourceMock, } from '../../mocks'; +import { Location, History } from 'history'; import { act } from 'react-dom/test-utils'; import { loadInitial } from './load_initial'; import { LensEmbeddableInput } from '../../embeddable'; @@ -65,7 +66,12 @@ describe('Mounter', () => { it('should initialize initial datasource', async () => { const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }); const lensStore = await makeLensStore({ data: services.data, @@ -79,8 +85,10 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput, + } ); }); expect(mockDatasource.initialize).toHaveBeenCalled(); @@ -88,7 +96,12 @@ describe('Mounter', () => { it('should have initialized only the initial datasource and visualization', async () => { const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }); const lensStore = await makeLensStore({ data: services.data, preloadedState }); await act(async () => { @@ -99,7 +112,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn() + { redirectCallback: jest.fn() } ); }); expect(mockDatasource.initialize).toHaveBeenCalled(); @@ -129,7 +142,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn() + { redirectCallback: jest.fn() } ); expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); }); @@ -170,7 +183,11 @@ describe('Mounter', () => { const emptyState = getPreloadedState(storeDeps) as LensAppState; services.attributeService.unwrapAttributes = jest.fn(); await act(async () => { - await loadInitial(lensStore, storeDeps, jest.fn(), undefined, emptyState); + await loadInitial(lensStore, storeDeps, { + redirectCallback: jest.fn(), + initialInput: undefined, + emptyState, + }); }); expect(lensStore.getState()).toEqual({ @@ -189,20 +206,28 @@ describe('Mounter', () => { it('loads a document and uses query and filters if initial input is provided', async () => { const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }); + const storeDeps = { + lensServices: services, + datasourceMap, + visualizationMap, + }; + const emptyState = getPreloadedState(storeDeps) as LensAppState; const lensStore = await makeLensStore({ data: services.data, preloadedState }); await act(async () => { - await loadInitial( - lensStore, - { - lensServices: services, - datasourceMap, - visualizationMap, - }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput - ); + await loadInitial(lensStore, storeDeps, { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + emptyState, + }); }); expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ @@ -235,8 +260,12 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + } ); }); @@ -248,8 +277,12 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + } ); }); @@ -263,8 +296,10 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: '5678' } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ savedObjectId: '5678' } as unknown) as LensEmbeddableInput, + } ); }); @@ -287,8 +322,12 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback, - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback, + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + } ); }); expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ @@ -298,6 +337,50 @@ describe('Mounter', () => { expect(redirectCallback).toHaveBeenCalled(); }); + it('redirects if saved object is an aliasMatch', async () => { + const services = makeDefaultServices(); + + const lensStore = makeLensStore({ data: services.data, preloadedState }); + + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'aliasMatch', + aliasTargetId: 'id2', + }, + }); + + await act(async () => { + await loadInitial( + lensStore, + { + lensServices: services, + datasourceMap, + visualizationMap, + }, + { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + history: { + location: { + search: '?search', + } as Location, + } as History, + } + ); + }); + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + + expect(services.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( + '#/edit/id2?search', + 'Lens visualization' + ); + }); + it('adds to the recently accessed list on load', async () => { const services = makeDefaultServices(); const lensStore = makeLensStore({ data: services.data, preloadedState }); @@ -309,8 +392,12 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + } ); }); diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 0be2bc9cfc00e..8ae6e58019c91 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -8,8 +8,10 @@ import { MiddlewareAPI } from '@reduxjs/toolkit'; import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { History } from 'history'; import { LensAppState, setState } from '..'; import { updateLayer, updateVisualizationState, LensStoreDeps } from '..'; +import { SharingSavedObjectProps } from '../../types'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; import { getInitialDatasourceId } from '../../utils'; import { initializeDatasources } from '../../editor_frame_service/editor_frame'; @@ -19,22 +21,50 @@ import { switchToSuggestion, } from '../../editor_frame_service/editor_frame/suggestion_helpers'; import { LensAppServices } from '../../app_plugin/types'; -import { getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; +import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; import { Document, injectFilterReferences } from '../../persistence'; export const getPersisted = async ({ initialInput, lensServices, + history, }: { initialInput: LensEmbeddableInput; lensServices: LensAppServices; -}): Promise<{ doc: Document } | undefined> => { - const { notifications, attributeService } = lensServices; + history?: History; +}): Promise< + { doc: Document; sharingSavedObjectProps: Omit } | undefined +> => { + const { notifications, spaces, attributeService } = lensServices; let doc: Document; try { - const attributes = await attributeService.unwrapAttributes(initialInput); - + const result = await attributeService.unwrapAttributes(initialInput); + if (!result) { + return { + doc: ({ + ...initialInput, + type: LENS_EMBEDDABLE_TYPE, + } as unknown) as Document, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }; + } + const { sharingSavedObjectProps, ...attributes } = result; + if (spaces && sharingSavedObjectProps?.outcome === 'aliasMatch' && history) { + // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash + const newObjectId = sharingSavedObjectProps?.aliasTargetId; // This is always defined if outcome === 'aliasMatch' + const newPath = lensServices.http.basePath.prepend( + `${getEditPath(newObjectId)}${history.location.search}` + ); + await spaces.ui.redirectLegacyUrl( + newPath, + i18n.translate('xpack.lens.legacyUrlConflict.objectNoun', { + defaultMessage: 'Lens visualization', + }) + ); + } doc = { ...initialInput, ...attributes, @@ -43,6 +73,10 @@ export const getPersisted = async ({ return { doc, + sharingSavedObjectProps: { + aliasTargetId: sharingSavedObjectProps?.aliasTargetId, + outcome: sharingSavedObjectProps?.outcome, + }, }; } catch (e) { notifications.toasts.addDanger( @@ -62,9 +96,17 @@ export function loadInitial( embeddableEditorIncomingState, initialContext, }: LensStoreDeps, - redirectCallback: (savedObjectId?: string) => void, - initialInput?: LensEmbeddableInput, - emptyState?: LensAppState + { + redirectCallback, + initialInput, + emptyState, + history, + }: { + redirectCallback: (savedObjectId?: string) => void; + initialInput?: LensEmbeddableInput; + emptyState?: LensAppState; + history?: History; + } ) { const { getState, dispatch } = store; const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices; @@ -146,11 +188,11 @@ export function loadInitial( redirectCallback(); }); } - getPersisted({ initialInput, lensServices }) + getPersisted({ initialInput, lensServices, history }) .then( (persisted) => { if (persisted) { - const { doc } = persisted; + const { doc, sharingSavedObjectProps } = persisted; if (attributeService.inputIsRefType(initialInput)) { lensServices.chrome.recentlyAccessed.add( getFullPath(initialInput.savedObjectId), @@ -190,6 +232,7 @@ export function loadInitial( dispatch( setState({ + sharingSavedObjectProps, query: doc.state.query, searchSessionId: dashboardFeatureFlag.allowByValueEmbeddables && diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 85cb79f6ea5da..6cf0529b34575 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -6,6 +6,7 @@ */ import { createSlice, current, PayloadAction } from '@reduxjs/toolkit'; +import { History } from 'history'; import { LensEmbeddableInput } from '..'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import { getInitialDatasourceId, getResolvedDateRange } from '../utils'; @@ -301,6 +302,7 @@ export const lensSlice = createSlice({ initialInput?: LensEmbeddableInput; redirectCallback: (savedObjectId?: string) => void; emptyState: LensAppState; + history: History; }> ) => state, }, diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 7321f72386b42..33f311a982f05 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -13,8 +13,7 @@ import { Document } from '../persistence'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import { DateRange } from '../../common'; import { LensAppServices } from '../app_plugin/types'; -import { DatasourceMap, VisualizationMap } from '../types'; - +import { DatasourceMap, VisualizationMap, SharingSavedObjectProps } from '../types'; export interface VisualizationState { activeId: string | null; state: unknown; @@ -44,6 +43,7 @@ export interface LensAppState extends EditorFrameState { savedQuery?: SavedQuery; searchSessionId: string; resolvedDateRange: DateRange; + sharingSavedObjectProps?: Omit; } export type DispatchSetState = ( diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 399e226a711db..844541cd2ad3e 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -256,7 +256,7 @@ export interface Datasource { ) => | Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; fixAction?: { label: string; newState: () => Promise }; }> | undefined; @@ -729,7 +729,7 @@ export interface Visualization { ) => | Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; }> | undefined; @@ -813,3 +813,9 @@ export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandle | LensTableRowContextMenuEvent ) => void; } + +export interface SharingSavedObjectProps { + outcome?: 'aliasMatch' | 'exactMatch' | 'conflict'; + aliasTargetId?: string; + errorJSON?: string; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 0a4b18f554f31..026c2827cedbd 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -383,7 +383,7 @@ export const getXyVisualization = ({ const errors: Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; }> = []; // check if the layers in the state are compatible with this type of chart @@ -488,7 +488,7 @@ function validateLayersForDimension( | { valid: true } | { valid: false; - payload: { shortMessage: string; longMessage: string }; + payload: { shortMessage: string; longMessage: React.ReactNode }; } { // Multiple layers must be consistent: // * either a dimension is missing in ALL of them diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 04d3838df2063..16287ae596df3 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -15,6 +15,7 @@ "../../../typings/**/*" ], "references": [ + { "path": "../spaces/tsconfig.json" }, { "path": "../../../src/core/tsconfig.json" }, { "path": "../task_manager/tsconfig.json" }, { "path": "../global_search/tsconfig.json"}, diff --git a/x-pack/plugins/ml/common/constants/messages.test.mock.ts b/x-pack/plugins/ml/common/constants/messages.test.mock.ts index 6e539617604c1..fbfff20adc5c2 100644 --- a/x-pack/plugins/ml/common/constants/messages.test.mock.ts +++ b/x-pack/plugins/ml/common/constants/messages.test.mock.ts @@ -78,4 +78,7 @@ export const nonBasicIssuesMessages = [ { id: 'missing_summary_count_field_name', }, + { + id: 'datafeed_preview_failed', + }, ]; diff --git a/x-pack/plugins/ml/common/constants/messages.test.ts b/x-pack/plugins/ml/common/constants/messages.test.ts index 59fc50757b674..c46eba458d1d2 100644 --- a/x-pack/plugins/ml/common/constants/messages.test.ts +++ b/x-pack/plugins/ml/common/constants/messages.test.ts @@ -173,6 +173,12 @@ describe('Constants: Messages parseMessages()', () => { text: 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', }, + { + id: 'datafeed_preview_failed', + status: 'error', + text: + 'The datafeed preview failed. This may be due to an error in the job or datafeed configurations.', + }, ]); }); }); diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index 0327e8746c7d8..fd3b9aa9d19b9 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -626,6 +626,30 @@ export const getMessages = once((docLinks?: DocLinksStart) => { 'the UNIX epoch beginning. Timestamps before 01/01/1970 00:00:00 (UTC) are not supported for machine learning jobs.', }), }, + datafeed_preview_no_documents: { + status: VALIDATION_STATUS.WARNING, + heading: i18n.translate( + 'xpack.ml.models.jobValidation.messages.datafeedPreviewNoDocumentsHeading', + { + defaultMessage: 'Datafeed preview', + } + ), + text: i18n.translate( + 'xpack.ml.models.jobValidation.messages.datafeedPreviewNoDocumentsMessage', + { + defaultMessage: + 'Running the datafeed preview over the current job configuration produces no results. ' + + 'If the index contains no documents this warning can be ignored, otherwise the job may be misconfigured.', + } + ), + }, + datafeed_preview_failed: { + status: VALIDATION_STATUS.ERROR, + text: i18n.translate('xpack.ml.models.jobValidation.messages.datafeedPreviewFailedMessage', { + defaultMessage: + 'The datafeed preview failed. This may be due to an error in the job or datafeed configurations.', + }), + }, }; }); diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index bd4b805baa186..509c74c359657 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -63,6 +63,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { const [exporting, setExporting] = useState(false); const [selectedJobType, setSelectedJobType] = useState(currentTab); const [switchTabConfirmVisible, setSwitchTabConfirmVisible] = useState(false); + const [switchTabNextTab, setSwitchTabNextTab] = useState(currentTab); const { displayErrorToast, displaySuccessToast } = useMemo( () => toastNotificationServiceProvider(toasts), [toasts] @@ -170,16 +171,23 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { } } - const attemptTabSwitch = useCallback(() => { - // if the user has already selected some jobs, open a confirm modal - // rather than changing tabs - if (selectedJobIds.length > 0) { - setSwitchTabConfirmVisible(true); - return; - } + const attemptTabSwitch = useCallback( + (jobType: JobType) => { + if (jobType === selectedJobType) { + return; + } + // if the user has already selected some jobs, open a confirm modal + // rather than changing tabs + if (selectedJobIds.length > 0) { + setSwitchTabNextTab(jobType); + setSwitchTabConfirmVisible(true); + return; + } - switchTab(); - }, [selectedJobIds]); + switchTab(jobType); + }, + [selectedJobIds] + ); useEffect(() => { setSelectedJobDependencies( @@ -187,10 +195,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { ); }, [selectedJobIds]); - function switchTab() { - const jobType = - selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector'; - + function switchTab(jobType: JobType) { setSwitchTabConfirmVisible(false); setSelectedJobIds([]); setSelectedJobType(jobType); @@ -211,7 +216,12 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { {showFlyout === true && isDisabled === false && ( <> - setShowFlyout(false)} hideCloseButton size="s"> + setShowFlyout(false)} + hideCloseButton + size="s" + data-test-subj="mlJobMgmtExportJobsFlyout" + >

@@ -227,8 +237,9 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { attemptTabSwitch('anomaly-detector')} disabled={exporting} + data-test-subj="mlJobMgmtExportJobsADTab" > = ({ isDisabled, currentTab }) => { attemptTabSwitch('data-frame-analytics')} disabled={exporting} + data-test-subj="mlJobMgmtExportJobsDFATab" > = ({ isDisabled, currentTab }) => { ) : ( <> - - + + {selectedJobIds.length === adJobIds.length ? ( + + ) : ( + + )} - {adJobIds.map((id) => ( -
- toggleSelectedJob(e.target.checked, id)} - /> - -
- ))} +
+ {adJobIds.map((id) => ( +
+ toggleSelectedJob(e.target.checked, id)} + /> + +
+ ))} +
)} @@ -284,26 +310,39 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { ) : ( <> - - + + {selectedJobIds.length === dfaJobIds.length ? ( + + ) : ( + + )} - - {dfaJobIds.map((id) => ( -
- toggleSelectedJob(e.target.checked, id)} - /> - -
- ))} +
+ {dfaJobIds.map((id) => ( +
+ toggleSelectedJob(e.target.checked, id)} + /> + +
+ ))} +
)} @@ -329,6 +368,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { disabled={selectedJobIds.length === 0 || exporting === true} onClick={onExport} fill + data-test-subj="mlJobMgmtExportExportButton" > = ({ isDisabled, currentTab }) => { {switchTabConfirmVisible === true ? ( switchTab(switchTabNextTab)} /> ) : null} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx index 732be345a1ee4..565ded9c6f6c3 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx @@ -30,6 +30,7 @@ export const CannotImportJobsCallout: FC = ({ jobs, autoExpand = false }) values: { num: jobs.length }, })} color="warning" + data-test-subj="mlJobMgmtImportJobsCannotBeImportedCallout" > {autoExpand ? ( diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx index 4c7a2471db9d6..70f94d1e03155 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx @@ -21,10 +21,12 @@ export const CannotReadFileCallout: FC = () => { })} color="warning" > - +
+ +
); diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index 68db42cdbf0eb..dfe07b1984e11 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -341,7 +341,12 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { {showFlyout === true && isDisabled === false && ( - +

@@ -373,22 +378,26 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { {showFileReadError ? : null} {totalJobsRead > 0 && jobType !== null && ( - <> +
{jobType === 'anomaly-detector' && ( - +
+ +
)} {jobType === 'data-frame-analytics' && ( - +
+ +
)} @@ -426,6 +435,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { value={jobId.jobId} onChange={(e) => renameJob(e.target.value, i)} isInvalid={jobId.jobIdValid === false} + data-test-subj="mlJobMgmtImportJobIdInput" /> @@ -465,7 +475,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => {
))} - + )} @@ -484,7 +494,12 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { - + = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobCreatorUpdate, jobValidator } = useContext(JobCreatorContext); + const [nextActive, setNextActive] = useState(false); if (jobCreator.type === JOB_TYPE.ADVANCED) { // for advanced jobs, ignore time range warning as the @@ -52,6 +53,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) // keep a record of the advanced validation in the jobValidator function setIsValid(valid: boolean) { jobValidator.advancedValid = valid; + setNextActive(valid); } return ( @@ -69,7 +71,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} next={() => setCurrentStep(WIZARD_STEPS.SUMMARY)} - nextActive={true} + nextActive={nextActive} /> )} diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index a5483491f1357..e890020eb726d 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -10,6 +10,7 @@ import { IScopedClusterClient } from 'kibana/server'; import { validateJob, ValidateJobPayload } from './job_validation'; import { ES_CLIENT_TOTAL_HITS_RELATION } from '../../../common/types/es_client'; import type { MlClient } from '../../lib/ml_client'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; const callAs = { fieldCaps: () => Promise.resolve({ body: { fields: [] } }), @@ -19,6 +20,8 @@ const callAs = { }), }; +const authHeader: AuthorizationHeader = {}; + const mlClusterClient = ({ asCurrentUser: callAs, asInternalUser: callAs, @@ -34,18 +37,19 @@ const mlClient = ({ }, }, }), + previewDatafeed: () => Promise.resolve({ body: [{}] }), } as unknown) as MlClient; // Note: The tests cast `payload` as any // so we can simulate possible runtime payloads // that don't satisfy the TypeScript specs. describe('ML - validateJob', () => { - it('basic validation messages', () => { + it('basic validation messages', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ @@ -58,14 +62,14 @@ describe('ML - validateJob', () => { }); const jobIdTests = (testIds: string[], messageId: string) => { - const promises = testIds.map((id) => { + const promises = testIds.map(async (id) => { const payload = ({ job: { analysis_config: { detectors: [] }, job_id: id, }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).catch(() => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).catch(() => { new Error('Promise should not fail for jobIdTests.'); }); }); @@ -86,7 +90,7 @@ describe('ML - validateJob', () => { job: { analysis_config: { detectors: [] }, groups: testIds }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes(messageId)).toBe(true); }); @@ -126,7 +130,7 @@ describe('ML - validateJob', () => { const payload = ({ job: { analysis_config: { bucket_span: format, detectors: [] } }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).catch(() => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).catch(() => { new Error('Promise should not fail for bucketSpanFormatTests.'); }); }); @@ -150,7 +154,7 @@ describe('ML - validateJob', () => { return bucketSpanFormatTests(validBucketSpanFormats, 'bucket_span_valid'); }); - it('at least one detector function is empty', () => { + it('at least one detector function is empty', async () => { const payload = ({ job: { analysis_config: { detectors: [] as Array<{ function?: string }> } }, } as unknown) as ValidateJobPayload; @@ -165,13 +169,13 @@ describe('ML - validateJob', () => { function: undefined, }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('detectors_function_empty')).toBe(true); }); }); - it('detector function is not empty', () => { + it('detector function is not empty', async () => { const payload = ({ job: { analysis_config: { detectors: [] as Array<{ function?: string }> } }, } as unknown) as ValidateJobPayload; @@ -179,37 +183,37 @@ describe('ML - validateJob', () => { function: 'count', }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('detectors_function_not_empty')).toBe(true); }); }); - it('invalid index fields', () => { + it('invalid index fields', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, fields: {}, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('index_fields_invalid')).toBe(true); }); }); - it('valid index fields', () => { + it('valid index fields', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, fields: { testField: {} }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('index_fields_valid')).toBe(true); }); }); - const getBasicPayload = (): any => ({ + const getBasicPayload = (): ValidateJobPayload => ({ job: { job_id: 'test', analysis_config: { @@ -231,7 +235,7 @@ describe('ML - validateJob', () => { const payload = getBasicPayload() as any; delete payload.job.analysis_config.influencers; - validateJob(mlClusterClient, mlClient, payload).then( + validateJob(mlClusterClient, mlClient, payload, authHeader).then( () => done( new Error('Promise should not resolve for this test when influencers is not an Array.') @@ -240,10 +244,10 @@ describe('ML - validateJob', () => { ); }); - it('detect duplicate detectors', () => { + it('detect duplicate detectors', async () => { const payload = getBasicPayload() as any; payload.job.analysis_config.detectors.push({ function: 'count' }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -256,7 +260,7 @@ describe('ML - validateJob', () => { }); }); - it('dedupe duplicate messages', () => { + it('dedupe duplicate messages', async () => { const payload = getBasicPayload() as any; // in this test setup, the following configuration passes // the duplicate detectors check, but would return the same @@ -266,7 +270,7 @@ describe('ML - validateJob', () => { { function: 'count', by_field_name: 'airline' }, { function: 'count', partition_field_name: 'airline' }, ]; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -278,9 +282,9 @@ describe('ML - validateJob', () => { }); }); - it('basic validation passes, extended checks return some messages', () => { + it('basic validation passes, extended checks return some messages', async () => { const payload = getBasicPayload(); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -291,8 +295,8 @@ describe('ML - validateJob', () => { }); }); - it('categorization job using mlcategory passes aggregatable field check', () => { - const payload: any = { + it('categorization job using mlcategory passes aggregatable field check', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -312,7 +316,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -325,8 +329,8 @@ describe('ML - validateJob', () => { }); }); - it('non-existent field reported as non aggregatable', () => { - const payload: any = { + it('non-existent field reported as non aggregatable', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -345,7 +349,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -357,8 +361,8 @@ describe('ML - validateJob', () => { }); }); - it('script field not reported as non aggregatable', () => { - const payload: any = { + it('script field not reported as non aggregatable', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -387,7 +391,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -399,4 +403,88 @@ describe('ML - validateJob', () => { ]); }); }); + + it('datafeed preview contains no docs', async () => { + const payload: ValidateJobPayload = { + job: { + job_id: 'categorization_test', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + function: 'count', + partition_field_name: 'custom_script_field', + }, + ], + influencers: [''], + }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [], + }, + }, + fields: { testField: {} }, + }; + + const mlClientEmptyDatafeedPreview = ({ + ...mlClient, + previewDatafeed: () => Promise.resolve({ body: [] }), + } as unknown) as MlClient; + + return validateJob(mlClusterClient, mlClientEmptyDatafeedPreview, payload, authHeader).then( + (messages) => { + const ids = messages.map((m) => m.id); + expect(ids).toStrictEqual([ + 'job_id_valid', + 'detectors_function_not_empty', + 'index_fields_valid', + 'field_not_aggregatable', + 'time_field_invalid', + 'datafeed_preview_no_documents', + ]); + } + ); + }); + + it('datafeed preview failed', async () => { + const payload: ValidateJobPayload = { + job: { + job_id: 'categorization_test', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + function: 'count', + partition_field_name: 'custom_script_field', + }, + ], + influencers: [''], + }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [], + }, + }, + fields: { testField: {} }, + }; + + const mlClientEmptyDatafeedPreview = ({ + ...mlClient, + previewDatafeed: () => Promise.reject({}), + } as unknown) as MlClient; + + return validateJob(mlClusterClient, mlClientEmptyDatafeedPreview, payload, authHeader).then( + (messages) => { + const ids = messages.map((m) => m.id); + expect(ids).toStrictEqual([ + 'job_id_valid', + 'detectors_function_not_empty', + 'index_fields_valid', + 'field_not_aggregatable', + 'time_field_invalid', + 'datafeed_preview_failed', + ]); + } + ); + }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 80eba7b864051..838f188455d44 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { IScopedClusterClient } from 'kibana/server'; +import type { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { fieldsServiceProvider } from '../fields_service'; import { getMessages, MessageId, JobValidationMessage } from '../../../common/constants/messages'; @@ -17,12 +17,14 @@ import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_ut import { validateBucketSpan } from './validate_bucket_span'; import { validateCardinality } from './validate_cardinality'; import { validateInfluencers } from './validate_influencers'; +import { validateDatafeedPreview } from './validate_datafeed_preview'; import { validateModelMemoryLimit } from './validate_model_memory_limit'; import { validateTimeRange, isValidTimeField } from './validate_time_range'; import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; -import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlClient } from '../../lib/ml_client'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; export type ValidateJobPayload = TypeOf; @@ -34,6 +36,7 @@ export async function validateJob( client: IScopedClusterClient, mlClient: MlClient, payload: ValidateJobPayload, + authHeader: AuthorizationHeader, isSecurityDisabled?: boolean ) { const messages = getMessages(); @@ -107,6 +110,8 @@ export async function validateJob( if (datafeedAggregations !== undefined && !job.analysis_config?.summary_count_field_name) { validationMessages.push({ id: 'missing_summary_count_field_name' }); } + + validationMessages.push(...(await validateDatafeedPreview(mlClient, authHeader, job))); } else { validationMessages = basicValidation.messages; validationMessages.push({ id: 'skipped_extended_tests' }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts b/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts new file mode 100644 index 0000000000000..e009dcf49fdab --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MlClient } from '../../lib/ml_client'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; +import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import type { JobValidationMessage } from '../../../common/constants/messages'; + +export async function validateDatafeedPreview( + mlClient: MlClient, + authHeader: AuthorizationHeader, + job: CombinedJob +): Promise { + const { datafeed_config: datafeed, ...tempJob } = job; + try { + const { body } = ((await mlClient.previewDatafeed( + { + body: { + job_config: tempJob, + datafeed_config: datafeed, + }, + }, + authHeader + // previewDatafeed response type is incorrect + )) as unknown) as { body: unknown[] }; + + if (Array.isArray(body) === false || body.length === 0) { + return [{ id: 'datafeed_preview_no_documents' }]; + } + return []; + } catch (error) { + return [{ id: 'datafeed_preview_failed' }]; + } +} diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 9309592dfc474..b75eab20e7bc0 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -8,9 +8,9 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; -import { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs'; +import type { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../types'; +import type { RouteInitialization } from '../types'; import { estimateBucketSpanSchema, modelMemoryLimitSchema, @@ -20,6 +20,7 @@ import { import { estimateBucketSpanFactory } from '../models/bucket_span_estimator'; import { calculateModelMemoryLimitProvider } from '../models/calculate_model_memory_limit'; import { validateJob, validateCardinality } from '../models/job_validation'; +import { getAuthorizationHeader } from '../lib/request_authorization'; import type { MlClient } from '../lib/ml_client'; type CalculateModelMemoryLimitPayload = TypeOf; @@ -192,6 +193,7 @@ export function jobValidationRoutes({ router, mlLicense, routeGuard }: RouteInit client, mlClient, request.body, + getAuthorizationHeader(request), mlLicense.isSecurityEnabled() === false ); diff --git a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts index 118d2e4140ced..27e1b6afe3364 100644 --- a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts @@ -52,7 +52,7 @@ export const datafeedConfigSchema = schema.object({ runtime_mappings: schema.maybe(schema.any()), scroll_size: schema.maybe(schema.number()), delayed_data_check_config: schema.maybe(schema.any()), - indices_options: indicesOptionsSchema, + indices_options: schema.maybe(indicesOptionsSchema), }); export const datafeedIdSchema = schema.object({ datafeedId: schema.string() }); diff --git a/x-pack/plugins/monitoring/public/application/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/global_state_context.tsx index dc33316dbd9d9..57bb638651d05 100644 --- a/x-pack/plugins/monitoring/public/application/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/global_state_context.tsx @@ -13,9 +13,11 @@ interface GlobalStateProviderProps { toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts']; } -interface State { +export interface State { cluster_uuid?: string; ccs?: any; + inSetupMode?: boolean; + save?: () => void; } export const GlobalStateContext = createContext({} as State); diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx index ddc097caea575..f329323bafda8 100644 --- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -15,8 +15,15 @@ import { TabMenuItem } from '../page_template'; import { PageLoading } from '../../../components'; import { Overview } from '../../../components/cluster/overview'; import { ExternalConfigContext } from '../../external_config_context'; +import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; const CODE_PATHS = [CODE_PATH_ALL]; +interface SetupModeProps { + setupMode: any; + flyoutComponent: any; + bottomBarComponent: any; +} export const ClusterOverview: React.FC<{}> = () => { // TODO: check how many requests with useClusters @@ -49,11 +56,20 @@ export const ClusterOverview: React.FC<{}> = () => { return ( {loaded ? ( - ( + + {flyoutComponent} + + {/* */} + {bottomBarComponent} + + )} /> ) : ( diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index f40c2d3ec5e50..29aafa09814fb 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; +import { EuiTab, EuiTabs } from '@elastic/eui'; import React from 'react'; import { useTitle } from '../hooks/use_title'; import { MonitoringToolbar } from '../../components/shared/toolbar'; @@ -29,34 +29,7 @@ export const PageTemplate: React.FC = ({ title, pageTitle, ta return (
- - - - -
{/* HERE GOES THE SETUP BUTTON */}
-
- - {pageTitle && ( -
- -

{pageTitle}

-
-
- )} -
-
-
- - - - -
- + {tabs && ( {tabs.map((item, idx) => { diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx new file mode 100644 index 0000000000000..70932e5177337 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx @@ -0,0 +1,200 @@ +/* + * 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 { render } from 'react-dom'; +import { get, includes } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { HttpStart } from 'kibana/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { Legacy } from '../../legacy_shims'; +import { SetupModeEnterButton } from '../../components/setup_mode/enter_button'; +import { SetupModeFeature } from '../../../common/enums'; +import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context'; +import { State as GlobalState } from '../../application/global_state_context'; + +function isOnPage(hash: string) { + return includes(window.location.hash, hash); +} + +let globalState: GlobalState; +let httpService: HttpStart; + +interface ISetupModeState { + enabled: boolean; + data: any; + callback?: (() => void) | null; + hideBottomBar: boolean; +} +const setupModeState: ISetupModeState = { + enabled: false, + data: null, + callback: null, + hideBottomBar: false, +}; + +export const getSetupModeState = () => setupModeState; + +export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => { + globalState.cluster_uuid = clusterUuid; + globalState.save?.(); +}; + +export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => { + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + + let url = '../api/monitoring/v1/setup/collection'; + if (uuid) { + url += `/node/${uuid}`; + } else if (!fetchWithoutClusterUuid && clusterUuid) { + url += `/cluster/${clusterUuid}`; + } else { + url += '/cluster'; + } + + try { + const response = await httpService.post(url, { + body: JSON.stringify({ + ccs, + }), + }); + return response; + } catch (err) { + // TODO: handle errors + throw new Error(err); + } +}; + +const notifySetupModeDataChange = () => setupModeState.callback && setupModeState.callback(); + +export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { + const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); + setupModeState.data = data; + const hasPermissions = get(data, '_meta.hasPermissions', false); + if (!hasPermissions) { + let text: string = ''; + if (!hasPermissions) { + text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { + defaultMessage: 'You do not have the necessary permissions to do this.', + }); + } + + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { + defaultMessage: 'Setup mode is not available', + }), + text, + }); + return toggleSetupMode(false); + } + notifySetupModeDataChange(); + + const clusterUuid = globalState.cluster_uuid; + if (!clusterUuid) { + const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); + const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( + (node: any) => node.isPartiallyMigrated || node.isFullyMigrated + ); + if (liveClusterUuid && migratedEsNodes.length > 0) { + setNewlyDiscoveredClusterUuid(liveClusterUuid); + } + } +}; + +export const hideBottomBar = () => { + setupModeState.hideBottomBar = true; + notifySetupModeDataChange(); +}; +export const showBottomBar = () => { + setupModeState.hideBottomBar = false; + notifySetupModeDataChange(); +}; + +export const disableElasticsearchInternalCollection = async () => { + const clusterUuid = globalState.cluster_uuid; + const url = `../api/monitoring/v1/setup/collection/${clusterUuid}/disable_internal_collection`; + try { + const response = await httpService.post(url); + return response; + } catch (err) { + // TODO: handle errors + throw new Error(err); + } +}; + +export const toggleSetupMode = (inSetupMode: boolean) => { + setupModeState.enabled = inSetupMode; + globalState.inSetupMode = inSetupMode; + globalState.save?.(); + setSetupModeMenuItem(); + notifySetupModeDataChange(); + + if (inSetupMode) { + // Intentionally do not await this so we don't block UI operations + updateSetupModeData(); + } +}; + +export const setSetupModeMenuItem = () => { + if (isOnPage('no-data')) { + return; + } + + const enabled = !globalState.inSetupMode; + const I18nContext = Legacy.shims.I18nContext; + + render( + + + + + , + document.getElementById('setupModeNav') + ); +}; + +export const initSetupModeState = async ( + state: GlobalState, + http: HttpStart, + callback?: () => void +) => { + globalState = state; + httpService = http; + if (callback) { + setupModeState.callback = callback; + } + + if (globalState.inSetupMode) { + toggleSetupMode(true); + } +}; + +export const isInSetupMode = (context?: ISetupModeContext) => { + if (context?.setupModeSupported === false) { + return false; + } + if (setupModeState.enabled) { + return true; + } + + return globalState.inSetupMode; +}; + +export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { + if (!setupModeState.enabled) { + return false; + } + + if (feature === SetupModeFeature.MetricbeatMigration) { + if (Legacy.shims.isCloud) { + return false; + } + } + + return true; +}; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts new file mode 100644 index 0000000000000..27462f07c07be --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const SetupModeRenderer: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js new file mode 100644 index 0000000000000..337dacd4ecae9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js @@ -0,0 +1,217 @@ +/* + * 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, { Fragment } from 'react'; +import { + getSetupModeState, + initSetupModeState, + updateSetupModeData, + disableElasticsearchInternalCollection, + toggleSetupMode, + setSetupModeMenuItem, +} from './setup_mode'; +import { Flyout } from '../../components/metricbeat_migration/flyout'; +import { + EuiBottomBar, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiTextColor, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import { findNewUuid } from '../../components/renderers/lib/find_new_uuid'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { GlobalStateContext } from '../../application/global_state_context'; +import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; + +class WrappedSetupModeRenderer extends React.Component { + globalState; + state = { + renderState: false, + isFlyoutOpen: false, + instance: null, + newProduct: null, + isSettingUpNew: false, + }; + + UNSAFE_componentWillMount() { + this.globalState = this.context; + const { kibana } = this.props; + initSetupModeState(this.globalState, kibana.services.http, (_oldData) => { + const newState = { renderState: true }; + + const { productName } = this.props; + if (!productName) { + this.setState(newState); + return; + } + + const setupModeState = getSetupModeState(); + if (!setupModeState.enabled || !setupModeState.data) { + this.setState(newState); + return; + } + + const data = setupModeState.data[productName]; + const oldData = _oldData ? _oldData[productName] : null; + if (data && oldData) { + const newUuid = findNewUuid(Object.keys(oldData.byUuid), Object.keys(data.byUuid)); + if (newUuid) { + newState.newProduct = data.byUuid[newUuid]; + } + } + + this.setState(newState); + }); + setSetupModeMenuItem(); + } + + reset() { + this.setState({ + renderState: false, + isFlyoutOpen: false, + instance: null, + newProduct: null, + isSettingUpNew: false, + }); + } + + getFlyout(data, meta) { + const { productName } = this.props; + const { isFlyoutOpen, instance, isSettingUpNew, newProduct } = this.state; + if (!data || !isFlyoutOpen) { + return null; + } + + let product = null; + if (newProduct) { + product = newProduct; + } + // For new instance discovery flow, we pass in empty instance object + else if (instance && Object.keys(instance).length) { + product = data.byUuid[instance.uuid]; + } + + if (!product) { + const uuids = Object.values(data.byUuid); + if (uuids.length && !isSettingUpNew) { + product = uuids[0]; + } else { + product = { + isNetNewUser: true, + }; + } + } + + return ( + this.reset()} + productName={productName} + product={product} + meta={meta} + instance={instance} + updateProduct={updateSetupModeData} + isSettingUpNew={isSettingUpNew} + /> + ); + } + + getBottomBar(setupModeState) { + if (!setupModeState.enabled || setupModeState.hideBottomBar) { + return null; + } + + return ( + + + + + + + + + , + }} + /> + + + + + + + + toggleSetupMode(false)} + > + {i18n.translate('xpack.monitoring.setupMode.exit', { + defaultMessage: `Exit setup mode`, + })} + + + + + + + + ); + } + + async shortcutToFinishMigration() { + await disableElasticsearchInternalCollection(); + await updateSetupModeData(); + } + + render() { + const { render, productName } = this.props; + const setupModeState = getSetupModeState(); + + let data = { byUuid: {} }; + if (setupModeState.data) { + if (productName && setupModeState.data[productName]) { + data = setupModeState.data[productName]; + } else if (setupModeState.data) { + data = setupModeState.data; + } + } + + const meta = setupModeState.data ? setupModeState.data._meta : null; + + return render({ + setupMode: { + data, + meta, + enabled: setupModeState.enabled, + productName, + updateSetupModeData, + shortcutToFinishMigration: () => this.shortcutToFinishMigration(), + openFlyout: (instance, isSettingUpNew) => + this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }), + closeFlyout: () => this.setState({ isFlyoutOpen: false }), + }, + flyoutComponent: this.getFlyout(data, meta), + bottomBarComponent: this.getBottomBar(setupModeState), + }); + } +} + +WrappedSetupModeRenderer.contextType = GlobalStateContext; +export const SetupModeRenderer = withKibana(WrappedSetupModeRenderer); diff --git a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx index 6e45d4d831ec9..e5962b7f80876 100644 --- a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx +++ b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx @@ -5,11 +5,21 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, OnRefreshChangeProps } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSuperDatePicker, + EuiTitle, + OnRefreshChangeProps, +} from '@elastic/eui'; import React, { useContext, useCallback } from 'react'; import { MonitoringTimeContainer } from '../../application/pages/use_monitoring_time'; -export const MonitoringToolbar = () => { +interface MonitoringToolbarProps { + pageTitle?: string; +} + +export const MonitoringToolbar: React.FC = ({ pageTitle }) => { const { currentTimerange, handleTimeChange, @@ -38,18 +48,36 @@ export const MonitoringToolbar = () => { ); return ( - - Setup Button + + + + +
{/* HERE GOES THE SETUP BUTTON */}
+
+ + {pageTitle && ( +
+ +

{pageTitle}

+
+
+ )} +
+
+
+ - {}} - isPaused={isPaused} - refreshInterval={refreshInterval} - onRefreshChange={onRefreshChange} - /> +
+ {}} + isPaused={isPaused} + refreshInterval={refreshInterval} + onRefreshChange={onRefreshChange} + /> +
); diff --git a/x-pack/plugins/monitoring/public/external_config.ts b/x-pack/plugins/monitoring/public/external_config.ts new file mode 100644 index 0000000000000..29ce410a5a9dc --- /dev/null +++ b/x-pack/plugins/monitoring/public/external_config.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +let config: { [key: string]: unknown } = {}; + +export const setConfig = (externalConfig: { [key: string]: unknown }) => { + config = externalConfig; +}; + +export const isReactMigrationEnabled = () => { + return config.renderReactApp; +}; diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index 28fd7494b1d10..f622f2944a31a 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -15,6 +15,8 @@ import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; import { SetupModeFeature } from '../../common/enums'; import { ISetupModeContext } from '../components/setup_mode/setup_mode_context'; +import * as setupModeReact from '../application/setup_mode/setup_mode'; +import { isReactMigrationEnabled } from '../external_config'; function isOnPage(hash: string) { return includes(window.location.hash, hash); @@ -209,6 +211,7 @@ export const initSetupModeState = async ($scope: any, $injector: any, callback?: }; export const isInSetupMode = (context?: ISetupModeContext) => { + if (isReactMigrationEnabled()) return setupModeReact.isInSetupMode(context); if (context?.setupModeSupported === false) { return false; } @@ -222,6 +225,7 @@ export const isInSetupMode = (context?: ISetupModeContext) => { }; export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { + if (isReactMigrationEnabled()) return setupModeReact.isSetupModeFeatureEnabled(feature); if (!setupModeState.enabled) { return false; } diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 6884dba760fcd..6f625194287ba 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -36,6 +36,7 @@ import { createThreadPoolRejectionsAlertType } from './alerts/thread_pool_reject import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; import { createCCRReadExceptionsAlertType } from './alerts/ccr_read_exceptions_alert'; import { createLargeShardSizeAlertType } from './alerts/large_shard_size_alert'; +import { setConfig } from './external_config'; interface MonitoringSetupPluginDependencies { home?: HomePublicPluginSetup; @@ -125,6 +126,7 @@ export class MonitoringPlugin }); const config = Object.fromEntries(externalConfig); + setConfig(config); if (config.renderReactApp) { const { renderApp } = await import('./application'); return renderApp(coreStart, pluginsStart, params, config); diff --git a/x-pack/plugins/reporting/server/routes/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations.ts index 874885e2258ae..d1d8302e394c3 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations.ts @@ -5,6 +5,7 @@ * 2.0. */ import { errors } from '@elastic/elasticsearch'; +import { SecurityHasPrivilegesIndexPrivilegesCheck } from '@elastic/elasticsearch/api/types'; import { RequestHandler } from 'src/core/server'; import { API_MIGRATE_ILM_POLICY_URL, @@ -36,10 +37,11 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log const { body } = await elasticsearch.client.asCurrentUser.security.hasPrivileges({ body: { index: [ - { + ({ privileges: ['manage'], // required to do anything with the reporting indices names: [store.getReportingIndexPattern()], - }, + allow_restricted_indices: true, + } as unknown) as SecurityHasPrivilegesIndexPrivilegesCheck, // TODO: Needed until `allow_restricted_indices` is added to the types. ], }, }); diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index 12e89f19e6248..4fab4ca72abab 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -16,6 +16,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "total": Object { + "type": "long", + }, + }, "_all": Object { "type": "long", }, @@ -62,6 +73,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "total": Object { + "type": "long", + }, + }, "_all": Object { "type": "long", }, @@ -117,6 +139,36 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "layout": Object { + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, + "total": Object { + "type": "long", + }, + }, "status": Object { "completed": Object { "type": "long", @@ -147,6 +199,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -180,6 +243,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "completed_with_warnings": Object { "PNG": Object { @@ -193,6 +267,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -226,6 +311,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "failed": Object { "PNG": Object { @@ -239,6 +335,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -272,6 +379,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "pending": Object { "PNG": Object { @@ -285,6 +403,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -318,6 +447,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "processing": Object { "PNG": Object { @@ -331,6 +471,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -364,6 +515,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, }, }, @@ -397,6 +559,36 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "layout": Object { + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, + "total": Object { + "type": "long", + }, + }, "status": Object { "completed": Object { "type": "long", @@ -427,6 +619,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -460,6 +663,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "completed_with_warnings": Object { "PNG": Object { @@ -473,6 +687,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -506,6 +731,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "failed": Object { "PNG": Object { @@ -519,6 +755,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -552,6 +799,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "pending": Object { "PNG": Object { @@ -565,6 +823,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -598,6 +867,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "processing": Object { "PNG": Object { @@ -611,6 +891,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -644,6 +935,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, }, }, diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 72824f6aeeb38..31ce6581d7de6 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -487,6 +487,7 @@ describe('data modeling', () => { // just check that the example objects can be cast to ReportingUsageType check({ PNG: { available: true, total: 7 }, + PNGV2: { available: true, total: 7 }, _all: 21, available: true, browser_type: 'chromium', @@ -495,6 +496,7 @@ describe('data modeling', () => { enabled: true, last7Days: { PNG: { available: true, total: 0 }, + PNGV2: { available: true, total: 0 }, _all: 0, csv: { available: true, total: 0 }, csv_searchsource: { available: true, total: 0 }, @@ -504,6 +506,12 @@ describe('data modeling', () => { layout: { preserve_layout: 0, print: 0 }, total: 0, }, + printable_pdf_v2: { + app: { dashboard: 0, visualization: 0 }, + available: true, + layout: { preserve_layout: 0, print: 0 }, + total: 0, + }, status: { completed: 0, failed: 0 }, statuses: {}, }, @@ -513,17 +521,26 @@ describe('data modeling', () => { layout: { preserve_layout: 7, print: 3 }, total: 10, }, + printable_pdf_v2: { + app: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, + available: true, + layout: { preserve_layout: 7, print: 3 }, + total: 10, + }, status: { completed: 21, failed: 0 }, statuses: { completed: { PNG: { dashboard: 3, visualization: 4 }, + PNGV2: { dashboard: 3, visualization: 4 }, csv: {}, printable_pdf: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, + printable_pdf_v2: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, }, }, }); check({ PNG: { available: true, total: 3 }, + PNGV2: { available: true, total: 3 }, _all: 4, available: true, browser_type: 'chromium', @@ -532,6 +549,7 @@ describe('data modeling', () => { enabled: true, last7Days: { PNG: { available: true, total: 3 }, + PNGV2: { available: true, total: 3 }, _all: 4, csv: { available: true, total: 0 }, csv_searchsource: { available: true, total: 0 }, @@ -541,6 +559,12 @@ describe('data modeling', () => { layout: { preserve_layout: 1, print: 0 }, total: 1, }, + printable_pdf_v2: { + app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, + available: true, + layout: { preserve_layout: 1, print: 0 }, + total: 1, + }, status: { completed: 4, failed: 0 }, statuses: { completed: { PNG: { visualization: 3 }, printable_pdf: { 'canvas workpad': 1 } }, @@ -552,6 +576,12 @@ describe('data modeling', () => { layout: { preserve_layout: 1, print: 0 }, total: 1, }, + printable_pdf_v2: { + app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, + available: true, + layout: { preserve_layout: 1, print: 0 }, + total: 1, + }, status: { completed: 4, failed: 0 }, statuses: { completed: { PNG: { visualization: 3 }, printable_pdf: { 'canvas workpad': 1 } }, @@ -571,9 +601,16 @@ describe('data modeling', () => { app: { dashboard: 0, visualization: 0 }, layout: { preserve_layout: 0, print: 0 }, }, + printable_pdf_v2: { + available: true, + total: 0, + app: { dashboard: 0, visualization: 0 }, + layout: { preserve_layout: 0, print: 0 }, + }, csv: { available: true, total: 0 }, csv_searchsource: { available: true, total: 0 }, PNG: { available: true, total: 0 }, + PNGV2: { available: true, total: 0 }, }, _all: 0, status: { completed: 0, failed: 0 }, @@ -584,9 +621,16 @@ describe('data modeling', () => { app: { dashboard: 0, visualization: 0 }, layout: { preserve_layout: 0, print: 0 }, }, + printable_pdf_v2: { + available: true, + total: 0, + app: { dashboard: 0, visualization: 0 }, + layout: { preserve_layout: 0, print: 0 }, + }, csv: { available: true, total: 0 }, csv_searchsource: { available: true, total: 0 }, PNG: { available: true, total: 0 }, + PNGV2: { available: true, total: 0 }, }); }); }); diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index 2060fdcb1f01e..54545dd23509b 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -25,7 +25,9 @@ const byAppCountsSchema: MakeSchemaFrom = { csv: appCountsSchema, csv_searchsource: appCountsSchema, PNG: appCountsSchema, + PNGV2: appCountsSchema, printable_pdf: appCountsSchema, + printable_pdf_v2: appCountsSchema, }; const availableTotalSchema: MakeSchemaFrom = { @@ -38,6 +40,7 @@ const jobTypesSchema: MakeSchemaFrom = { csv: availableTotalSchema, csv_searchsource: availableTotalSchema, PNG: availableTotalSchema, + PNGV2: availableTotalSchema, printable_pdf: { ...availableTotalSchema, app: appCountsSchema, @@ -46,6 +49,14 @@ const jobTypesSchema: MakeSchemaFrom = { preserve_layout: { type: 'long' }, }, }, + printable_pdf_v2: { + ...availableTotalSchema, + app: appCountsSchema, + layout: { + print: { type: 'long' }, + preserve_layout: { type: 'long' }, + }, + }, }; const rangeStatsSchema: MakeSchemaFrom = { diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index aae8c0ff42710..389dc27c46c66 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -63,7 +63,13 @@ export interface AvailableTotal { deprecated?: number; } -type BaseJobTypes = 'csv' | 'csv_searchsource' | 'PNG' | 'printable_pdf'; +type BaseJobTypes = + | 'csv' + | 'csv_searchsource' + | 'PNG' + | 'PNGV2' + | 'printable_pdf' + | 'printable_pdf_v2'; export interface LayoutCounts { print: number; @@ -80,6 +86,11 @@ export type JobTypes = { [K in BaseJobTypes]: AvailableTotal } & { app: AppCounts; layout: LayoutCounts; }; +} & { + printable_pdf_v2: AvailableTotal & { + app: AppCounts; + layout: LayoutCounts; + }; }; export type ByAppCounts = { [J in BaseJobTypes]?: AppCounts }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 259a9e9e8de38..48f3a81a00af2 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -162,7 +162,6 @@ export const createLifecycleExecutor = ( > = { alertWithLifecycle: ({ id, fields }) => { currentAlerts[id] = fields; - return alertInstanceFactory(id); }, }; @@ -179,7 +178,6 @@ export const createLifecycleExecutor = ( const currentAlertIds = Object.keys(currentAlerts); const trackedAlertIds = Object.keys(state.trackedAlerts); const newAlertIds = currentAlertIds.filter((alertId) => !trackedAlertIds.includes(alertId)); - const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; const trackedAlertStates = Object.values(state.trackedAlerts); @@ -188,9 +186,10 @@ export const createLifecycleExecutor = ( `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)` ); - const alertsDataMap: Record> = { - ...currentAlerts, - }; + const trackedAlertsDataMap: Record< + string, + { indexName: string; fields: Partial } + > = {}; if (trackedAlertStates.length) { const { hits } = await ruleDataClient.getReader().search({ @@ -228,59 +227,77 @@ export const createLifecycleExecutor = ( hits.hits.forEach((hit) => { const fields = parseTechnicalFields(hit.fields); + const indexName = hit._index; const alertId = fields[ALERT_INSTANCE_ID]; - alertsDataMap[alertId] = { - ...commonRuleFields, - ...fields, + trackedAlertsDataMap[alertId] = { + indexName, + fields, }; }); } - const eventsToIndex = allAlertIds.map((alertId) => { - const alertData = alertsDataMap[alertId]; - - if (!alertData) { - logger.warn(`Could not find alert data for ${alertId}`); - } - - const isNew = !state.trackedAlerts[alertId]; - const isRecovered = !currentAlerts[alertId]; - const isActive = !isRecovered; - - const { alertUuid, started } = state.trackedAlerts[alertId] ?? { - alertUuid: v4(), - started: commonRuleFields[TIMESTAMP], - }; - const event: ParsedTechnicalFields = { - ...alertData, - ...commonRuleFields, - [ALERT_DURATION]: (options.startedAt.getTime() - new Date(started).getTime()) * 1000, - [ALERT_INSTANCE_ID]: alertId, - [ALERT_START]: started, - [ALERT_STATUS]: isActive ? ALERT_STATUS_ACTIVE : ALERT_STATUS_RECOVERED, - [ALERT_WORKFLOW_STATUS]: alertData[ALERT_WORKFLOW_STATUS] ?? 'open', - [ALERT_UUID]: alertUuid, - [EVENT_KIND]: 'signal', - [EVENT_ACTION]: isNew ? 'open' : isActive ? 'active' : 'close', - [VERSION]: ruleDataClient.kibanaVersion, - ...(isRecovered ? { [ALERT_END]: commonRuleFields[TIMESTAMP] } : {}), - }; - - return event; - }); + const makeEventsDataMapFor = (alertIds: string[]) => + alertIds.map((alertId) => { + const alertData = trackedAlertsDataMap[alertId]; + const currentAlertData = currentAlerts[alertId]; + + if (!alertData) { + logger.warn(`Could not find alert data for ${alertId}`); + } + + const isNew = !state.trackedAlerts[alertId]; + const isRecovered = !currentAlerts[alertId]; + const isActive = !isRecovered; + + const { alertUuid, started } = state.trackedAlerts[alertId] ?? { + alertUuid: v4(), + started: commonRuleFields[TIMESTAMP], + }; + + const event: ParsedTechnicalFields = { + ...alertData?.fields, + ...commonRuleFields, + ...currentAlertData, + [ALERT_DURATION]: (options.startedAt.getTime() - new Date(started).getTime()) * 1000, + + [ALERT_INSTANCE_ID]: alertId, + [ALERT_START]: started, + [ALERT_UUID]: alertUuid, + [ALERT_STATUS]: isRecovered ? ALERT_STATUS_RECOVERED : ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: alertData?.fields[ALERT_WORKFLOW_STATUS] ?? 'open', + [EVENT_KIND]: 'signal', + [EVENT_ACTION]: isNew ? 'open' : isActive ? 'active' : 'close', + [VERSION]: ruleDataClient.kibanaVersion, + ...(isRecovered ? { [ALERT_END]: commonRuleFields[TIMESTAMP] } : {}), + }; + + return { + indexName: alertData?.indexName, + event, + }; + }); + + const trackedEventsToIndex = makeEventsDataMapFor(trackedAlertIds); + const newEventsToIndex = makeEventsDataMapFor(newAlertIds); + const allEventsToIndex = [...trackedEventsToIndex, ...newEventsToIndex]; - if (eventsToIndex.length > 0 && ruleDataClient.isWriteEnabled()) { - logger.debug(`Preparing to index ${eventsToIndex.length} alerts.`); + if (allEventsToIndex.length > 0 && ruleDataClient.isWriteEnabled()) { + logger.debug(`Preparing to index ${allEventsToIndex.length} alerts.`); await ruleDataClient.getWriter().bulk({ - body: eventsToIndex.flatMap((event) => [{ index: { _id: event[ALERT_UUID]! } }, event]), + body: allEventsToIndex.flatMap(({ event, indexName }) => [ + indexName + ? { index: { _id: event[ALERT_UUID]!, _index: indexName, require_alias: false } } + : { index: { _id: event[ALERT_UUID]! } }, + event, + ]), }); } const nextTrackedAlerts = Object.fromEntries( - eventsToIndex - .filter((event) => event[ALERT_STATUS] !== 'closed') - .map((event) => { + allEventsToIndex + .filter(({ event }) => event[ALERT_STATUS] !== 'closed') + .map(({ event }) => { const alertId = event[ALERT_INSTANCE_ID]!; const alertUuid = event[ALERT_UUID]!; const started = new Date(event[ALERT_START]!).toISOString(); diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index d38f963a60c33..a7ef6b34616c3 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -318,6 +318,52 @@ describe('AuthenticationService', () => { }); }); + describe('getServerBaseURL()', () => { + let getServerBaseURL: () => string; + beforeEach(() => { + mockStartAuthenticationParams.http.getServerInfo.mockReturnValue({ + name: 'some-name', + protocol: 'socket', + hostname: 'test-hostname', + port: 1234, + }); + + service.setup(mockSetupAuthenticationParams); + service.start(mockStartAuthenticationParams); + + getServerBaseURL = jest.requireMock('./authenticator').Authenticator.mock.calls[0][0] + .getServerBaseURL; + }); + + it('falls back to legacy server config if `public` config is not specified', async () => { + expect(getServerBaseURL()).toBe('socket://test-hostname:1234'); + }); + + it('respects `public` config if it is specified', async () => { + mockStartAuthenticationParams.config.public = { + protocol: 'https', + } as ConfigType['public']; + expect(getServerBaseURL()).toBe('https://test-hostname:1234'); + + mockStartAuthenticationParams.config.public = { + hostname: 'elastic.co', + } as ConfigType['public']; + expect(getServerBaseURL()).toBe('socket://elastic.co:1234'); + + mockStartAuthenticationParams.config.public = { + port: 4321, + } as ConfigType['public']; + expect(getServerBaseURL()).toBe('socket://test-hostname:4321'); + + mockStartAuthenticationParams.config.public = { + protocol: 'https', + hostname: 'elastic.co', + port: 4321, + } as ConfigType['public']; + expect(getServerBaseURL()).toBe('https://elastic.co:4321'); + }); + }); + describe('getCurrentUser()', () => { let getCurrentUser: (r: KibanaRequest) => AuthenticatedUser | null; beforeEach(async () => { diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index 79dcfb8d804b2..538bc26e6ffe3 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -41,7 +41,7 @@ interface AuthenticationServiceSetupParams { } interface AuthenticationServiceStartParams { - http: Pick; + http: Pick; config: ConfigType; clusterClient: IClusterClient; legacyAuditLogger: SecurityAuditLogger; @@ -234,6 +234,17 @@ export class AuthenticationService { license: this.license, }); + /** + * Retrieves server protocol name/host name/port and merges it with `xpack.security.public` config + * to construct a server base URL (deprecated, used by the SAML provider only). + */ + const getServerBaseURL = () => { + const { protocol, hostname, port } = http.getServerInfo(); + const serverConfig = { protocol, hostname, port, ...config.public }; + + return `${serverConfig.protocol}://${serverConfig.hostname}:${serverConfig.port}`; + }; + const getCurrentUser = (request: KibanaRequest) => http.auth.get(request).state ?? null; @@ -247,6 +258,7 @@ export class AuthenticationService { config: { authc: config.authc }, getCurrentUser, featureUsageService, + getServerBaseURL, license: this.license, session, }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index ca33be92e9e99..27dfd89a31756 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -55,6 +55,7 @@ function getMockOptions({ basePath: httpServiceMock.createSetupContract().basePath, license: licenseMock.create(), loggers: loggingSystemMock.create(), + getServerBaseURL: jest.fn(), config: createConfig( ConfigSchema.validate({ authc: { selector, providers, http } }), loggingSystemMock.create().get(), diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 4eeadf23c50b2..5252f5c618f97 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -87,6 +87,7 @@ export interface AuthenticatorOptions { loggers: LoggerFactory; clusterClient: IClusterClient; session: PublicMethodsOf; + getServerBaseURL: () => string; } // Mapping between provider key defined in the config and authentication @@ -216,6 +217,7 @@ export class Authenticator { client: this.options.clusterClient.asInternalUser, logger: this.options.loggers.get('tokens'), }), + getServerBaseURL: this.options.getServerBaseURL, }; this.providers = new Map( diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index 5d3417ae9db11..6554b525fc9e0 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -17,6 +17,7 @@ export type MockAuthenticationProviderOptions = ReturnType< export function mockAuthenticationProviderOptions(options?: { name: string }) { return { + getServerBaseURL: () => 'test-protocol://test-hostname:1234', client: elasticsearchServiceMock.createClusterClient(), logger: loggingSystemMock.create().get(), basePath: httpServiceMock.createBasePath(), diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index f6d9af24ee1ad..d5b173fcfad8c 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -26,6 +26,7 @@ import type { Tokens } from '../tokens'; */ export interface AuthenticationProviderOptions { name: string; + getServerBaseURL: () => string; basePath: HttpServiceSetup['basePath']; getRequestOriginalURL: ( request: KibanaRequest, diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index 4a32383d18dec..251a59228fb03 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -39,23 +39,7 @@ describe('SAMLAuthenticationProvider', () => { ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', - }); - }); - - it('throws if `realm` option is not specified', () => { - const providerOptions = mockAuthenticationProviderOptions(); - - expect(() => new SAMLAuthenticationProvider(providerOptions)).toThrowError( - 'Realm name must be specified' - ); - expect(() => new SAMLAuthenticationProvider(providerOptions, {})).toThrowError( - 'Realm name must be specified' - ); - expect(() => new SAMLAuthenticationProvider(providerOptions, { realm: '' })).toThrowError( - 'Realm name must be specified' - ); + provider = new SAMLAuthenticationProvider(mockOptions); }); describe('`login` method', () => { @@ -67,6 +51,7 @@ describe('SAMLAuthenticationProvider', () => { body: { access_token: 'some-token', refresh_token: 'some-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) @@ -108,13 +93,13 @@ describe('SAMLAuthenticationProvider', () => { body: { access_token: 'some-token', refresh_token: 'some-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) ); provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: true, }); await expect( @@ -169,6 +154,10 @@ describe('SAMLAuthenticationProvider', () => { it('fails if realm from state is different from the realm provider is configured with.', async () => { const request = httpServerMock.createKibanaRequest(); + const customMockOptions = mockAuthenticationProviderOptions({ name: 'saml' }); + provider = new SAMLAuthenticationProvider(customMockOptions, { + realm: 'test-realm', + }); await expect( provider.login( @@ -184,7 +173,7 @@ describe('SAMLAuthenticationProvider', () => { ) ); - expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); + expect(customMockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('redirects to the default location if state contains empty redirect URL.', async () => { @@ -195,6 +184,7 @@ describe('SAMLAuthenticationProvider', () => { body: { access_token: 'user-initiated-login-token', refresh_token: 'user-initiated-login-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) @@ -232,13 +222,13 @@ describe('SAMLAuthenticationProvider', () => { body: { access_token: 'user-initiated-login-token', refresh_token: 'user-initiated-login-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) ); provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: true, }); await expect( @@ -275,6 +265,7 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.asInternalUser.transport.request.mockResolvedValue( securityMock.createApiResponse({ body: { + realm: 'test-realm', access_token: 'idp-initiated-login-token', refresh_token: 'idp-initiated-login-refresh-token', authentication: mockUser, @@ -301,7 +292,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/authenticate', - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + body: { ids: [], content: 'saml-response-xml' }, }); }); @@ -342,20 +333,19 @@ describe('SAMLAuthenticationProvider', () => { username: 'user', access_token: 'valid-token', refresh_token: 'valid-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) ); provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: true, }); }); it('redirects to the home page if `useRelayStateDeepLink` is set to `false`.', async () => { provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: false, }); @@ -454,10 +444,39 @@ describe('SAMLAuthenticationProvider', () => { ) ); }); + + it('uses `realm` name instead of `acs` if it is specified for SAML authenticate request.', async () => { + // Create new provider instance with additional `realm` option. + provider = new SAMLAuthenticationProvider(mockOptions, { + realm: 'test-realm', + }); + + await expect( + provider.login(httpServerMock.createKibanaRequest({ headers: {} }), { + type: SAMLLogin.LoginWithSAMLResponse, + samlResponse: 'saml-response-xml', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo(`${mockOptions.basePath.serverBasePath}/`, { + state: { + accessToken: 'valid-token', + refreshToken: 'valid-refresh-token', + realm: 'test-realm', + }, + user: mockUser, + }) + ); + + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + }); + }); }); describe('IdP initiated login with existing session', () => { - it('returns `notHandled` if new SAML Response is rejected.', async () => { + it('fails if new SAML Response is rejected and provider is not configured with specific realm.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = 'Bearer some-valid-token'; @@ -466,6 +485,39 @@ describe('SAMLAuthenticationProvider', () => { ); mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + { + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + realm: 'test-realm', + } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + + expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml' }, + }); + }); + + it('returns `notHandled` if new SAML Response is rejected and provider is configured with specific realm.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const authorization = 'Bearer some-valid-token'; + + provider = new SAMLAuthenticationProvider(mockOptions, { + realm: 'test-realm', + }); + + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 503, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); + await expect( provider.login( request, @@ -521,7 +573,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/authenticate', - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + body: { ids: [], content: 'saml-response-xml' }, }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); @@ -543,7 +595,7 @@ describe('SAMLAuthenticationProvider', () => { ), ], [ - 'current session is is expired', + 'current session is expired', Promise.reject( new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ), @@ -568,6 +620,7 @@ describe('SAMLAuthenticationProvider', () => { username: 'user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) @@ -595,7 +648,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/authenticate', - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + body: { ids: [], content: 'saml-response-xml' }, }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); @@ -624,6 +677,7 @@ describe('SAMLAuthenticationProvider', () => { username: 'user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) @@ -632,7 +686,6 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: true, }); @@ -661,7 +714,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/authenticate', - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + body: { ids: [], content: 'saml-response-xml' }, }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); @@ -699,19 +752,16 @@ describe('SAMLAuthenticationProvider', () => { body: { id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', + realm: 'test-realm', }, }) ); await expect( - provider.login( - request, - { - type: SAMLLogin.LoginInitiatedByUser, - redirectURL: '/test-base-path/some-path#some-fragment', - }, - { realm: 'test-realm' } - ) + provider.login(request, { + type: SAMLLogin.LoginInitiatedByUser, + redirectURL: '/test-base-path/some-path#some-fragment', + }) ).resolves.toEqual( AuthenticationResult.redirectTo( 'https://idp-host/path/login?SAMLRequest=some%20request%20', @@ -728,7 +778,9 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: 'test-realm' }, + body: { + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); expect(mockOptions.logger.warn).not.toHaveBeenCalled(); @@ -742,6 +794,7 @@ describe('SAMLAuthenticationProvider', () => { body: { id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', + realm: 'test-realm', }, }) ); @@ -771,19 +824,32 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: 'test-realm' }, + body: { + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); expect(mockOptions.logger.warn).not.toHaveBeenCalled(); }); - it('fails if SAML request preparation fails.', async () => { - const request = httpServerMock.createKibanaRequest(); + it('uses `realm` name instead of `acs` if it is specified for SAML prepare request.', async () => { + const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); - const failureReason = new errors.ResponseError( - securityMock.createApiResponse({ statusCode: 401, body: {} }) + // Create new provider instance with additional `realm` option. + const customMockOptions = mockAuthenticationProviderOptions(); + provider = new SAMLAuthenticationProvider(customMockOptions, { + realm: 'test-realm', + }); + + customMockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + id: 'some-request-id', + redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', + realm: 'test-realm', + }, + }) ); - mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.login( @@ -794,12 +860,47 @@ describe('SAMLAuthenticationProvider', () => { }, { realm: 'test-realm' } ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path#some-fragment', + realm: 'test-realm', + }, + } + ) + ); + + expect(customMockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/prepare', + body: { realm: 'test-realm' }, + }); + }); + + it('fails if SAML request preparation fails.', async () => { + const request = httpServerMock.createKibanaRequest(); + + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 401, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); + + await expect( + provider.login(request, { + type: SAMLLogin.LoginInitiatedByUser, + redirectURL: '/test-base-path/some-path#some-fragment', + }) ).resolves.toEqual(AuthenticationResult.failed(failureReason)); expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: 'test-realm' }, + body: { + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); }); @@ -893,7 +994,6 @@ describe('SAMLAuthenticationProvider', () => { state: { requestId: 'some-request-id', redirectURL: '/mock-server-basepath/s/foo/some-path#some-fragment', - realm: 'test-realm', }, } ) @@ -905,7 +1005,9 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: 'test-realm' }, + body: { + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); @@ -1112,6 +1214,13 @@ describe('SAMLAuthenticationProvider', () => { it('fails if realm from state is different from the realm provider is configured with.', async () => { const request = httpServerMock.createKibanaRequest(); + + // Create new provider instance with additional `realm` option. + const customMockOptions = mockAuthenticationProviderOptions({ name: 'saml' }); + provider = new SAMLAuthenticationProvider(customMockOptions, { + realm: 'test-realm', + }); + await expect(provider.authenticate(request, { realm: 'other-realm' })).resolves.toEqual( AuthenticationResult.failed( Boom.unauthorized( @@ -1186,7 +1295,10 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { + query_string: 'SAMLRequest=xxx%20yyy', + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); @@ -1305,7 +1417,10 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { + query_string: 'SAMLRequest=xxx%20yyy', + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); @@ -1324,7 +1439,10 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { + query_string: 'SAMLRequest=xxx%20yyy', + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index 37e7e868e4d3d..6eab0c5dc4875 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -42,9 +42,10 @@ interface ProviderState extends Partial { redirectURL?: string; /** - * The name of the SAML realm that was used to establish session. + * The name of the SAML realm that was used to establish session (may not be known during URL + * fragment capturing stage). */ - realm: string; + realm?: string; } /** @@ -105,9 +106,10 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { static readonly type = 'saml'; /** - * Specifies Elasticsearch SAML realm name that Kibana should use. + * Optionally specifies Elasticsearch SAML realm name that Kibana should use. If not specified + * Kibana ACS URL is used for realm matching instead. */ - private readonly realm: string; + private readonly realm?: string; /** * Indicates if we should treat non-empty `RelayState` as a deep link in Kibana we should redirect @@ -121,12 +123,8 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { ) { super(options); - if (!samlOptions || !samlOptions.realm) { - throw new Error('Realm name must be specified'); - } - - this.realm = samlOptions.realm; - this.useRelayStateDeepLink = samlOptions.useRelayStateDeepLink ?? false; + this.realm = samlOptions?.realm; + this.useRelayStateDeepLink = samlOptions?.useRelayStateDeepLink ?? false; } /** @@ -144,7 +142,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // It may happen that Kibana is re-configured to use different realm for the same provider name, // we should clear such session an log user out. - if (state?.realm && state.realm !== this.realm) { + if (state && this.realm && state.realm !== this.realm) { const message = `State based on realm "${state.realm}", but provider with the name "${this.options.name}" is configured to use realm "${this.realm}".`; this.logger.debug(message); return AuthenticationResult.failed(Boom.unauthorized(message)); @@ -215,7 +213,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // It may happen that Kibana is re-configured to use different realm for the same provider name, // we should clear such session an log user out. - if (state?.realm && state.realm !== this.realm) { + if (state && this.realm && state.realm !== this.realm) { const message = `State based on realm "${state.realm}", but provider with the name "${this.options.name}" is configured to use realm "${this.realm}".`; this.logger.debug(message); return AuthenticationResult.failed(Boom.unauthorized(message)); @@ -274,7 +272,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // and state !== undefined). In this case case it'd be safer to trigger SP initiated logout // for the new session as well. const redirect = isIdPInitiatedSLORequest - ? await this.performIdPInitiatedSingleLogout(request) + ? await this.performIdPInitiatedSingleLogout(request, this.realm || state?.realm) : state ? await this.performUserInitiatedSingleLogout(state.accessToken!, state.refreshToken!) : // Once Elasticsearch can consume logout response we'll be sending it here. See https://github.com/elastic/elasticsearch/issues/40901 @@ -331,9 +329,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // If we have a `SAMLResponse` and state, but state doesn't contain all the necessary information, // then something unexpected happened and we should fail. - const { requestId: stateRequestId, redirectURL: stateRedirectURL } = state || { + const { + requestId: stateRequestId, + redirectURL: stateRedirectURL, + realm: stateRealm, + } = state || { requestId: '', redirectURL: '', + realm: '', }; if (state && !stateRequestId) { const message = 'SAML response state does not have corresponding request id.'; @@ -349,7 +352,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { : 'Login has been initiated by Identity Provider.' ); - let result: { access_token: string; refresh_token: string; authentication: AuthenticationInfo }; + const providerRealm = this.realm || stateRealm; + + let result: { + access_token: string; + refresh_token: string; + realm: string; + authentication: AuthenticationInfo; + }; try { // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/authenticate`. @@ -362,7 +372,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { body: { ids: !isIdPInitiatedLogin ? [stateRequestId] : [], content: samlResponse, - realm: this.realm, + ...(providerRealm ? { realm: providerRealm } : {}), }, }) ).body as any; @@ -372,7 +382,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // Since we don't know upfront what realm is targeted by the Identity Provider initiated login // there is a chance that it failed because of realm mismatch and hence we should return // `notHandled` and give other SAML providers a chance to properly handle it instead. - return isIdPInitiatedLogin + return isIdPInitiatedLogin && providerRealm ? AuthenticationResult.notHandled() : AuthenticationResult.failed(err); } @@ -404,7 +414,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { state: { accessToken: result.access_token, refreshToken: result.refresh_token, - realm: this.realm, + realm: result.realm, }, user: this.authenticationInfoToAuthenticatedUser(result.authentication), } @@ -545,7 +555,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { authHeaders: { authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(), }, - state: { accessToken, refreshToken, realm: this.realm }, + state: { accessToken, refreshToken, realm: this.realm || state.realm }, } ); } @@ -559,15 +569,18 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Trying to initiate SAML handshake.'); try { + // Prefer realm name if it's specified, otherwise fallback to ACS. + const preparePayload = this.realm ? { realm: this.realm } : { acs: this.getACS() }; + // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/prepare`. // We can replace generic `transport.request` with a dedicated API method call once // https://github.com/elastic/elasticsearch/issues/67189 is resolved. - const { id: requestId, redirect } = ( + const { id: requestId, redirect, realm } = ( await this.options.client.asInternalUser.transport.request({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: this.realm }, + body: preparePayload, }) ).body as any; @@ -575,7 +588,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // Store request id in the state so that we can reuse it once we receive `SAMLResponse`. return AuthenticationResult.redirectTo(redirect, { - state: { requestId, redirectURL, realm: this.realm }, + state: { requestId, redirectURL, realm }, }); } catch (err) { this.logger.debug(`Failed to initiate SAML handshake: ${getDetailedErrorMessage(err)}`); @@ -612,10 +625,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { * Calls `saml/invalidate` with the `SAMLRequest` query string parameter received from the Identity * Provider and redirects user back to the Identity Provider if needed. * @param request Request instance. + * @param realm Configured SAML realm name. */ - private async performIdPInitiatedSingleLogout(request: KibanaRequest) { + private async performIdPInitiatedSingleLogout(request: KibanaRequest, realm?: string) { this.logger.debug('Single logout has been initiated by the Identity Provider.'); + // Prefer realm name if it's specified, otherwise fallback to ACS. + const invalidatePayload = realm ? { realm } : { acs: this.getACS() }; + // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/invalidate`. // We can replace generic `transport.request` with a dedicated API method call once @@ -627,7 +644,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // Elasticsearch expects `query_string` without leading `?`, so we should strip it with `slice`. body: { query_string: request.url.search ? request.url.search.slice(1) : '', - realm: this.realm, + ...invalidatePayload, }, }) ).body as any; @@ -637,6 +654,15 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { return redirect; } + /** + * Constructs and returns Kibana's Assertion consumer service URL. + */ + private getACS() { + return `${this.options.getServerBaseURL()}${ + this.options.basePath.serverBasePath + }/api/security/v1/saml`; + } + /** * Tries to initiate SAML authentication handshake. If the request already includes user URL hash fragment, we will * initiate handshake right away, otherwise we'll redirect user to a dedicated page where we capture URL hash fragment diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 75dfcb6151ea7..3be565d59a11f 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -58,6 +58,7 @@ describe('config schema', () => { "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "loginAssistanceMessage": "", + "public": Object {}, "secureCookies": false, "session": Object { "cleanupInterval": "PT1H", @@ -109,6 +110,7 @@ describe('config schema', () => { "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "loginAssistanceMessage": "", + "public": Object {}, "secureCookies": false, "session": Object { "cleanupInterval": "PT1H", @@ -159,6 +161,7 @@ describe('config schema', () => { "cookieName": "sid", "enabled": true, "loginAssistanceMessage": "", + "public": Object {}, "secureCookies": false, "session": Object { "cleanupInterval": "PT1H", @@ -179,6 +182,109 @@ describe('config schema', () => { ); }); + describe('public', () => { + it('properly validates `protocol`', async () => { + expect(ConfigSchema.validate({ public: { protocol: 'http' } }).public).toMatchInlineSnapshot(` + Object { + "protocol": "http", + } + `); + + expect(ConfigSchema.validate({ public: { protocol: 'https' } }).public) + .toMatchInlineSnapshot(` + Object { + "protocol": "https", + } + `); + + expect(() => ConfigSchema.validate({ public: { protocol: 'ftp' } })) + .toThrowErrorMatchingInlineSnapshot(` + "[public.protocol]: types that failed validation: + - [public.protocol.0]: expected value to equal [http] + - [public.protocol.1]: expected value to equal [https]" + `); + + expect(() => ConfigSchema.validate({ public: { protocol: 'some-protocol' } })) + .toThrowErrorMatchingInlineSnapshot(` + "[public.protocol]: types that failed validation: + - [public.protocol.0]: expected value to equal [http] + - [public.protocol.1]: expected value to equal [https]" + `); + }); + + it('properly validates `hostname`', async () => { + expect(ConfigSchema.validate({ public: { hostname: 'elastic.co' } }).public) + .toMatchInlineSnapshot(` + Object { + "hostname": "elastic.co", + } + `); + + expect(ConfigSchema.validate({ public: { hostname: '192.168.1.1' } }).public) + .toMatchInlineSnapshot(` + Object { + "hostname": "192.168.1.1", + } + `); + + expect(ConfigSchema.validate({ public: { hostname: '::1' } }).public).toMatchInlineSnapshot(` + Object { + "hostname": "::1", + } + `); + + expect(() => + ConfigSchema.validate({ public: { hostname: 'http://elastic.co' } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.hostname]: value must be a valid hostname (see RFC 1123)."` + ); + + expect(() => + ConfigSchema.validate({ public: { hostname: 'localhost:5601' } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.hostname]: value must be a valid hostname (see RFC 1123)."` + ); + }); + + it('properly validates `port`', async () => { + expect(ConfigSchema.validate({ public: { port: 1234 } }).public).toMatchInlineSnapshot(` + Object { + "port": 1234, + } + `); + + expect(ConfigSchema.validate({ public: { port: 0 } }).public).toMatchInlineSnapshot(` + Object { + "port": 0, + } + `); + + expect(ConfigSchema.validate({ public: { port: 65535 } }).public).toMatchInlineSnapshot(` + Object { + "port": 65535, + } + `); + + expect(() => + ConfigSchema.validate({ public: { port: -1 } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.port]: Value must be equal to or greater than [0]."` + ); + + expect(() => + ConfigSchema.validate({ public: { port: 65536 } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.port]: Value must be equal to or lower than [65535]."` + ); + + expect(() => + ConfigSchema.validate({ public: { port: '56x1' } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.port]: expected value of type [number] but got [string]"` + ); + }); + }); + describe('authc.oidc', () => { it(`returns a validation error when authc.providers is "['oidc']" and realm is unspecified`, async () => { expect(() => ConfigSchema.validate({ authc: { providers: ['oidc'] } })).toThrow( @@ -255,14 +361,42 @@ describe('config schema', () => { }); describe('authc.saml', () => { - it('fails if authc.providers includes `saml`, but `saml.realm` is not specified', async () => { - expect(() => ConfigSchema.validate({ authc: { providers: ['saml'] } })).toThrow( - '[authc.saml.realm]: expected value of type [string] but got [undefined]' - ); + it('does not fail if authc.providers includes `saml`, but `saml.realm` is not specified', async () => { + expect(ConfigSchema.validate({ authc: { providers: ['saml'] } }).authc) + .toMatchInlineSnapshot(` + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "providers": Array [ + "saml", + ], + "saml": Object {}, + "selector": Object {}, + } + `); - expect(() => ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } })).toThrow( - '[authc.saml.realm]: expected value of type [string] but got [undefined]' - ); + expect(ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } }).authc) + .toMatchInlineSnapshot(` + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "providers": Array [ + "saml", + ], + "saml": Object {}, + "selector": Object {}, + } + `); expect( ConfigSchema.validate({ diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 9daf0aff4c6cb..90fccf4bc6c26 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -228,6 +228,11 @@ export const ConfigSchema = schema.object({ sameSiteCookies: schema.maybe( schema.oneOf([schema.literal('Strict'), schema.literal('Lax'), schema.literal('None')]) ), + public: schema.object({ + protocol: schema.maybe(schema.oneOf([schema.literal('http'), schema.literal('https')])), + hostname: schema.maybe(schema.string({ hostname: true })), + port: schema.maybe(schema.number({ min: 0, max: 65535 })), + }), authc: schema.object({ selector: schema.object({ enabled: schema.maybe(schema.boolean()) }), providers: schema.oneOf([schema.arrayOf(schema.string()), providersConfigSchema], { @@ -256,7 +261,7 @@ export const ConfigSchema = schema.object({ saml: providerOptionsSchema( 'saml', schema.object({ - realm: schema.string(), + realm: schema.maybe(schema.string()), maxRedirectURLSize: schema.maybe(schema.byteSize()), }) ), diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index ad6f81eaeefff..4518679755156 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -286,7 +286,7 @@ describe('Config Deprecations', () => { const { messages } = applyConfigDeprecations(cloneDeep(config)); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.authc.providers.saml..maxRedirectURLSize\\" is is no longer used.", + "\\"xpack.security.authc.providers.saml..maxRedirectURLSize\\" is no longer used.", ] `); }); diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index f68112760632e..169211184a325 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -13,6 +13,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ unused, }) => [ rename('sessionTimeout', 'session.idleTimeout'), + rename('authProviders', 'authc.providers'), rename('audit.appender.kind', 'audit.appender.type'), rename('audit.appender.layout.kind', 'audit.appender.layout.type'), @@ -121,7 +122,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ }), message: i18n.translate('xpack.security.deprecations.maxRedirectURLSizeMessage', { defaultMessage: - '"xpack.security.authc.providers.saml..maxRedirectURLSize" is is no longer used.', + '"xpack.security.authc.providers.saml..maxRedirectURLSize" is no longer used.', }), correctiveActions: { manualSteps: [ diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts index 98cb7729c9440..69fce914cb1d5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -23,8 +23,8 @@ export const EndpointActionLogRequestSchema = { query: schema.object({ page: schema.number({ defaultValue: 1, min: 1 }), page_size: schema.number({ defaultValue: 10, min: 1, max: 100 }), - start_date: schema.maybe(schema.string()), - end_date: schema.maybe(schema.string()), + start_date: schema.string(), + end_date: schema.string(), }), params: schema.object({ agent_id: schema.string(), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index d49868aae9227..c6d30825c21c9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -65,8 +65,8 @@ export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; export interface ActivityLog { page: number; pageSize: number; - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; data: ActivityLogEntry[]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index e179c02987462..3c277d1d4019b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { EuiLoadingContent, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; @@ -369,11 +368,7 @@ export const AlertsTableComponent: React.FC = ({ }, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]); if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { - return ( - - - - ); + return null; } return ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 4c3db2ae62be3..4d4ac102ea645 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -695,7 +695,7 @@ const RuleDetailsPageComponent: React.FC = ({ enabled={isExistingRule && (rule?.enabled ?? false)} onChange={handleOnChangeEnabledRule} /> - {i18n.ACTIVATED_RULE} + {i18n.ACTIVATE_RULE}
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index ca3e5a4587a09..a83647f8a9781 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -28,10 +28,10 @@ export const EXPERIMENTAL = i18n.translate( } ); -export const ACTIVATED_RULE = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.activatedRuleLabel', +export const ACTIVATE_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.activateRuleLabel', { - defaultMessage: 'Activated', + defaultMessage: 'Activate', } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 0ac73df6704c8..9c557f83012bf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -126,6 +126,8 @@ export const endpointActivityLogHttpMock = httpHandlerMockFactory => { disabled: false, page: 1, pageSize: 50, - startDate: undefined, - endDate: undefined, + startDate: 'now-1d', + endDate: 'now', isInvalidDateRange: false, + autoRefreshOptions: { + enabled: false, + duration: DEFAULT_POLL_INTERVAL, + }, + recentlyUsedDateRanges: [], }, logData: createUninitialisedResourceState(), }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 7fbe2dfc0a099..49ba88fd47717 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -48,7 +48,14 @@ describe('EndpointList store concerns', () => { disabled: false, page: 1, pageSize: 50, + startDate: 'now-1d', + endDate: 'now', isInvalidDateRange: false, + autoRefreshOptions: { + enabled: false, + duration: DEFAULT_POLL_INTERVAL, + }, + recentlyUsedDateRanges: [], }, logData: { type: 'UninitialisedResourceState' }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index e51fe15e7130f..83d3e62cf98f2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -267,6 +267,8 @@ describe('endpoint list middleware', () => { payload: { page, pageSize: 50, + startDate: 'now-1d', + endDate: 'now', }, }); }; @@ -311,6 +313,8 @@ describe('endpoint list middleware', () => { expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledWith({ path: expect.any(String), query: { + end_date: 'now', + start_date: 'now-1d', page: 1, page_size: 50, }, @@ -396,6 +400,8 @@ describe('endpoint list middleware', () => { query: { page: 3, page_size: 50, + start_date: 'now-1d', + end_date: 'now', }, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index df4361a6048a8..6b88183db6841 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -640,12 +640,12 @@ async function endpointDetailsActivityLogChangedMiddleware({ }); try { - const { page, pageSize } = getActivityLogDataPaging(getState()); + const { page, pageSize, startDate, endDate } = getActivityLogDataPaging(getState()); const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { agent_id: selectedAgent(getState()), }); const activityLog = await coreStart.http.get(route, { - query: { page, page_size: pageSize }, + query: { page, page_size: pageSize, start_date: startDate, end_date: endDate }, }); dispatch({ type: 'endpointDetailsActivityLogChanged', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 02d2adce833cf..b16caf00b4e28 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -24,6 +24,7 @@ import { AppAction } from '../../../../common/store/actions'; import { ImmutableReducer } from '../../../../common/store'; import { Immutable } from '../../../../../common/endpoint/types'; import { createUninitialisedResourceState, isUninitialisedResourceState } from '../../../state'; +import { DEFAULT_POLL_INTERVAL } from '../../../common/constants'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -172,7 +173,11 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta }, }, }; - } else if (action.type === 'endpointDetailsActivityLogUpdatePaging') { + } else if ( + action.type === 'endpointDetailsActivityLogUpdatePaging' || + action.type === 'endpointDetailsActivityLogUpdateIsInvalidDateRange' || + action.type === 'userUpdatedActivityLogRefreshOptions' + ) { return { ...state, endpointDetails: { @@ -186,7 +191,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta }, }, }; - } else if (action.type === 'endpointDetailsActivityLogUpdateIsInvalidDateRange') { + } else if (action.type === 'userUpdatedActivityLogRecentlyUsedDateRanges') { return { ...state, endpointDetails: { @@ -195,7 +200,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...state.endpointDetails.activityLog, paging: { ...state.endpointDetails.activityLog.paging, - ...action.payload, + recentlyUsedDateRanges: action.payload, }, }, }, @@ -315,9 +320,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta logData: createUninitialisedResourceState(), paging: { disabled: false, + isInvalidDateRange: false, page: 1, pageSize: 50, - isInvalidDateRange: false, + startDate: 'now-1d', + endDate: 'now', + autoRefreshOptions: { + enabled: false, + duration: DEFAULT_POLL_INTERVAL, + }, + recentlyUsedDateRanges: [], }, }; @@ -337,7 +349,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - activityLog, + activityLog: { + ...activityLog, + paging: { + ...activityLog.paging, + startDate: state.endpointDetails.activityLog.paging.startDate, + endDate: state.endpointDetails.activityLog.paging.endDate, + recentlyUsedDateRanges: + state.endpointDetails.activityLog.paging.recentlyUsedDateRanges, + }, + }, hostDetails: { ...state.endpointDetails.hostDetails, detailsError: undefined, @@ -355,7 +376,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - activityLog, + activityLog: { + ...activityLog, + paging: { + ...activityLog.paging, + startDate: state.endpointDetails.activityLog.paging.startDate, + endDate: state.endpointDetails.activityLog.paging.endDate, + recentlyUsedDateRanges: + state.endpointDetails.activityLog.paging.recentlyUsedDateRanges, + }, + }, hostDetails: { ...state.endpointDetails.hostDetails, detailsLoading: !isNotLoadingDetails, @@ -372,7 +402,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - activityLog, + activityLog: { + ...activityLog, + paging: { + ...activityLog.paging, + startDate: state.endpointDetails.activityLog.paging.startDate, + endDate: state.endpointDetails.activityLog.paging.endDate, + recentlyUsedDateRanges: + state.endpointDetails.activityLog.paging.recentlyUsedDateRanges, + }, + }, hostDetails: { ...state.endpointDetails.hostDetails, detailsLoading: true, @@ -391,7 +430,15 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - activityLog, + activityLog: { + ...activityLog, + paging: { + ...activityLog.paging, + startDate: state.endpointDetails.activityLog.paging.startDate, + endDate: state.endpointDetails.activityLog.paging.endDate, + recentlyUsedDateRanges: state.endpointDetails.activityLog.paging.recentlyUsedDateRanges, + }, + }, hostDetails: { ...state.endpointDetails.hostDetails, detailsError: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 82057af233e43..dd0bc79f1ba52 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { EuiSuperDatePickerRecentRange } from '@elastic/eui'; import { ActivityLog, HostInfo, @@ -41,9 +42,14 @@ export interface EndpointState { disabled?: boolean; page: number; pageSize: number; - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; isInvalidDateRange: boolean; + autoRefreshOptions: { + enabled: boolean; + duration: number; + }; + recentlyUsedDateRanges: EuiSuperDatePickerRecentRange[]; }; logData: AsyncResourceState; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts index fa2aaaa16ae37..ee723bd0bf0f5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts @@ -10,12 +10,14 @@ import { getIsInvalidDateRange } from './utils'; describe('utils', () => { describe('getIsInvalidDateRange', () => { - it('should return FALSE when either dates are undefined', () => { - expect(getIsInvalidDateRange({})).toBe(false); - expect(getIsInvalidDateRange({ startDate: moment().subtract(1, 'd').toISOString() })).toBe( - false - ); - expect(getIsInvalidDateRange({ endDate: moment().toISOString() })).toBe(false); + it('should return FALSE when startDate is before endDate', () => { + expect(getIsInvalidDateRange({ startDate: 'now-1d', endDate: 'now' })).toBe(false); + expect( + getIsInvalidDateRange({ + startDate: moment().subtract(1, 'd').toISOString(), + endDate: moment().toISOString(), + }) + ).toBe(false); }); it('should return TRUE when startDate is after endDate', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts index e2d619743c83b..1bfb99c68ef66 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import dateMath from '@elastic/datemath'; import moment from 'moment'; import { HostInfo, HostMetadata } from '../../../../common/endpoint/types'; @@ -29,12 +30,12 @@ export const getIsInvalidDateRange = ({ startDate, endDate, }: { - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; }) => { - if (startDate && endDate) { - const start = moment(startDate); - const end = moment(endDate); + const start = moment(dateMath.parse(startDate)); + const end = moment(dateMath.parse(endDate)); + if (start.isValid() && end.isValid()) { return start.isAfter(end); } return false; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx index e921078539303..30ab082559c7b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx @@ -8,95 +8,140 @@ import { useDispatch } from 'react-redux'; import React, { memo, useCallback } from 'react'; import styled from 'styled-components'; -import moment from 'moment'; -import { EuiFlexGroup, EuiFlexItem, EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; +import dateMath from '@elastic/datemath'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSuperDatePicker, + EuiSuperDatePickerRecentRange, +} from '@elastic/eui'; -import * as i18 from '../../../translations'; import { useEndpointSelector } from '../../../hooks'; -import { getActivityLogDataPaging } from '../../../../store/selectors'; +import { + getActivityLogDataPaging, + getActivityLogRequestLoading, +} from '../../../../store/selectors'; +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../../../common/constants'; +import { useUiSetting$ } from '../../../../../../../common/lib/kibana'; + +interface Range { + from: string; + to: string; + display: string; +} const DatePickerWrapper = styled.div` width: ${(props) => props.theme.eui.fractions.single.percentage}; - background: white; + max-width: 350px; `; const StickyFlexItem = styled(EuiFlexItem)` - max-width: 350px; + background: ${(props) => `${props.theme.eui.euiHeaderBackgroundColor}`}; position: sticky; - top: ${(props) => props.theme.eui.euiSizeM}; + top: 0; z-index: 1; - padding: ${(props) => `0 ${props.theme.eui.paddingSizes.m}`}; + padding: ${(props) => `${props.theme.eui.paddingSizes.m}`}; `; export const DateRangePicker = memo(() => { const dispatch = useDispatch(); - const { page, pageSize, startDate, endDate, isInvalidDateRange } = useEndpointSelector( - getActivityLogDataPaging - ); + const { + page, + pageSize, + startDate, + endDate, + autoRefreshOptions, + recentlyUsedDateRanges, + } = useEndpointSelector(getActivityLogDataPaging); + + const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading); - const onChangeStartDate = useCallback( - (date) => { + const dispatchActionUpdateActivityLogPaging = useCallback( + async ({ start, end }) => { dispatch({ type: 'endpointDetailsActivityLogUpdatePaging', payload: { disabled: false, page, pageSize, - startDate: date ? date?.toISOString() : undefined, - endDate: endDate ? endDate : undefined, + startDate: dateMath.parse(start)?.toISOString(), + endDate: dateMath.parse(end)?.toISOString(), }, }); }, - [dispatch, endDate, page, pageSize] + [dispatch, page, pageSize] ); - const onChangeEndDate = useCallback( - (date) => { + const onRefreshChange = useCallback( + (evt) => { dispatch({ - type: 'endpointDetailsActivityLogUpdatePaging', + type: 'userUpdatedActivityLogRefreshOptions', payload: { - disabled: false, - page, - pageSize, - startDate: startDate ? startDate : undefined, - endDate: date ? date.toISOString() : undefined, + autoRefreshOptions: { enabled: !evt.isPaused, duration: evt.refreshInterval }, }, }); }, - [dispatch, startDate, page, pageSize] + [dispatch] ); + const onRefresh = useCallback(() => { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + page, + pageSize, + startDate, + endDate, + }, + }); + }, [dispatch, page, pageSize, startDate, endDate]); + + const onTimeChange = useCallback( + ({ start: newStart, end: newEnd }) => { + const newRecentlyUsedDateRanges = [ + { start: newStart, end: newEnd }, + ...recentlyUsedDateRanges + .filter( + (recentlyUsedRange) => + !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) + ) + .slice(0, 9), + ]; + dispatch({ + type: 'userUpdatedActivityLogRecentlyUsedDateRanges', + payload: newRecentlyUsedDateRanges, + }); + + dispatchActionUpdateActivityLogPaging({ start: newStart, end: newEnd }); + }, + [dispatch, recentlyUsedDateRanges, dispatchActionUpdateActivityLogPaging] + ); + + const [quickRanges] = useUiSetting$(DEFAULT_TIMEPICKER_QUICK_RANGES); + const commonlyUsedRanges = !quickRanges.length + ? [] + : quickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); + return ( - - + + - - } - endDateControl={ - - } + diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 5172b59450e03..f0b6b5fbc8962 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -9,11 +9,13 @@ import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { + EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiEmptyPrompt, + EuiSpacer, } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { LogEntry } from './components/log_entry'; @@ -114,6 +116,17 @@ export const EndpointActivityLog = memo( <> + {!isPagingDisabled && activityLogLoaded && !activityLogData.length && ( + <> + + + + )} {activityLogLoaded && activityLogData.map((logEntry) => ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx index 372bd4491d7d4..123a51e5a52bd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx @@ -22,6 +22,8 @@ export const dummyEndpointActivityLog = ( data: { page: 1, pageSize: 50, + startDate: moment().subtract(5, 'day').fromNow().toString(), + endDate: moment().toString(), data: [ { type: 'action', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 996198568ad27..ea999334ee771 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -10,6 +10,8 @@ import * as reactTestingLibrary from '@testing-library/react'; import { EndpointList } from './index'; import '../../../../common/mock/match_media'; +import { createUseUiSetting$Mock } from '../../../../../public/common/lib/kibana/kibana_react.mock'; + import { mockEndpointDetailsApiResult, mockEndpointResultList, @@ -28,7 +30,7 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_da import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; import { mockPolicyResultList } from '../../policy/store/test_mock_utils'; import { getEndpointDetailsPath } from '../../../common/routing'; -import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kibana'; +import { KibanaServices, useKibana, useToasts, useUiSetting$ } from '../../../../common/lib/kibana'; import { hostIsolationHttpMocks } from '../../../../common/lib/endpoint_isolation/mocks'; import { createFailedResourceState, @@ -40,7 +42,11 @@ import { import { getCurrentIsolationRequestState } from '../store/selectors'; import { licenseService } from '../../../../common/hooks/use_license'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; -import { APP_PATH, MANAGEMENT_PATH } from '../../../../../common/constants'; +import { + APP_PATH, + MANAGEMENT_PATH, + DEFAULT_TIMEPICKER_QUICK_RANGES, +} from '../../../../../common/constants'; import { TransformStats, TRANSFORM_STATE } from '../types'; import { metadataTransformPrefix } from '../../../../../common/endpoint/constants'; @@ -63,6 +69,59 @@ jest.mock('../../policy/store/services/ingest', () => { sendGetEndpointSecurityPackage: () => Promise.resolve({}), }; }); +const mockUseUiSetting$ = useUiSetting$ as jest.Mock; +const timepickerRanges = [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, +]; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/hooks/use_license'); @@ -759,6 +818,14 @@ describe('when on the endpoint list page', () => { disconnect: jest.fn(), })); + mockUseUiSetting$.mockImplementation((key, defaultValue) => { + const useUiSetting$Mock = createUseUiSetting$Mock(); + + return key === DEFAULT_TIMEPICKER_QUICK_RANGES + ? [timepickerRanges, jest.fn()] + : useUiSetting$Mock(key, defaultValue); + }); + const fleetActionGenerator = new FleetActionGenerator('seed'); const responseData = fleetActionGenerator.generateResponse({ agent_id: agentId, @@ -766,9 +833,12 @@ describe('when on the endpoint list page', () => { const actionData = fleetActionGenerator.generate({ agents: [agentId], }); + getMockData = () => ({ page: 1, pageSize: 50, + startDate: 'now-1d', + endDate: 'now', data: [ { type: 'response', @@ -838,7 +908,7 @@ describe('when on the endpoint list page', () => { expect(emptyState).not.toBe(null); }); - it('should display empty state when no log data', async () => { + it('should not display empty state when no log data', async () => { const activityLogTab = await renderResult.findByTestId('activity_log'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(activityLogTab); @@ -848,36 +918,39 @@ describe('when on the endpoint list page', () => { dispatchEndpointDetailsActivityLogChanged('success', { page: 1, pageSize: 50, + startDate: 'now-1d', + endDate: 'now', data: [], }); }); const emptyState = await renderResult.queryByTestId('activityLogEmpty'); - expect(emptyState).not.toBe(null); + expect(emptyState).toBe(null); + + const superDatePicker = await renderResult.queryByTestId('activityLogSuperDatePicker'); + expect(superDatePicker).not.toBe(null); }); - it('should not display empty state with no log data while date range filter is active', async () => { - const activityLogTab = await renderResult.findByTestId('activity_log'); + it('should display activity log when tab is loaded using the URL', async () => { + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { - reactTestingLibrary.fireEvent.click(activityLogTab); + history.push( + `${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=activity_log` + ); }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_endpoint=1&show=activity_log' + ); await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); reactTestingLibrary.act(() => { - dispatchEndpointDetailsActivityLogChanged('success', { - page: 1, - pageSize: 50, - startDate: new Date().toISOString(), - data: [], - }); + dispatchEndpointDetailsActivityLogChanged('success', getMockData()); }); - - const emptyState = await renderResult.queryByTestId('activityLogEmpty'); - const dateRangePicker = await renderResult.queryByTestId('activityLogDateRangePicker'); - expect(emptyState).toBe(null); - expect(dateRangePicker).not.toBe(null); + const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + expect(logEntries.length).toEqual(2); }); - it('should display activity log when tab is loaded using the URL', async () => { + it('should display a callout message if no log data', async () => { const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { history.push( @@ -890,10 +963,17 @@ describe('when on the endpoint list page', () => { ); await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); reactTestingLibrary.act(() => { - dispatchEndpointDetailsActivityLogChanged('success', getMockData()); + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [], + }); }); - const logEntries = await renderResult.queryAllByTestId('timelineEntry'); - expect(logEntries.length).toEqual(2); + + const activityLogCallout = await renderResult.findByTestId('activityLogNoDataCallout'); + expect(activityLogCallout).not.toBeNull(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index 57ad3e4808bd5..c8a29eed3fda7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -15,20 +15,6 @@ export const ACTIVITY_LOG = { tabTitle: i18n.translate('xpack.securitySolution.endpointDetails.activityLog', { defaultMessage: 'Activity Log', }), - datePicker: { - startDate: i18n.translate( - 'xpack.securitySolution.endpointDetails.activityLog.datePicker.startDate', - { - defaultMessage: 'Pick a start date', - } - ), - endDate: i18n.translate( - 'xpack.securitySolution.endpointDetails.activityLog.datePicker.endDate', - { - defaultMessage: 'Pick an end date', - } - ), - }, LogEntry: { endOfLog: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.endOfLog', @@ -36,6 +22,13 @@ export const ACTIVITY_LOG = { defaultMessage: 'Nothing more to show', } ), + dateRangeMessage: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.dateRangeMessage.title', + { + defaultMessage: + 'Nothing to show for selected date range, please select another and try again.', + } + ), emptyState: { title: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.title', diff --git a/x-pack/plugins/security_solution/server/config.test.ts b/x-pack/plugins/security_solution/server/config.test.ts new file mode 100644 index 0000000000000..67956acd6656f --- /dev/null +++ b/x-pack/plugins/security_solution/server/config.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { configSchema } from './config'; + +describe('config', () => { + describe('alertIgnoreFields', () => { + test('should default to an empty array', () => { + expect(configSchema.validate({}).alertIgnoreFields).toEqual([]); + }); + + test('should accept an array of strings', () => { + expect( + configSchema.validate({ alertIgnoreFields: ['foo.bar', 'mars.bar'] }).alertIgnoreFields + ).toEqual(['foo.bar', 'mars.bar']); + }); + + test('should throw if a non string is being sent in', () => { + expect( + () => + configSchema.validate({ + alertIgnoreFields: 5, + }).alertIgnoreFields + ).toThrow('[alertIgnoreFields]: expected value of type [array] but got [number]'); + }); + + test('should throw if we send in an invalid regular expression as a string', () => { + expect( + () => + configSchema.validate({ + alertIgnoreFields: ['/(/'], + }).alertIgnoreFields + ).toThrow( + '[alertIgnoreFields]: "Invalid regular expression: /(/: Unterminated group" at array position 0' + ); + }); + + test('should throw with two errors if we send two invalid regular expressions', () => { + expect( + () => + configSchema.validate({ + alertIgnoreFields: ['/(/', '/(invalid/'], + }).alertIgnoreFields + ).toThrow( + '[alertIgnoreFields]: "Invalid regular expression: /(/: Unterminated group" at array position 0. "Invalid regular expression: /(invalid/: Unterminated group" at array position 1' + ); + }); + + test('should throw with two errors with a valid string mixed in if we send two invalid regular expressions', () => { + expect( + () => + configSchema.validate({ + alertIgnoreFields: ['/(/', 'valid.string', '/(invalid/'], + }).alertIgnoreFields + ).toThrow( + '[alertIgnoreFields]: "Invalid regular expression: /(/: Unterminated group" at array position 0. "Invalid regular expression: /(invalid/: Unterminated group" at array position 2' + ); + }); + + test('should accept a valid regular expression within the string', () => { + expect( + configSchema.validate({ + alertIgnoreFields: ['/(.*)/'], + }).alertIgnoreFields + ).toEqual(['/(.*)/']); + }); + + test('should accept two valid regular expressions', () => { + expect( + configSchema.validate({ + alertIgnoreFields: ['/(.*)/', '/(.valid*)/'], + }).alertIgnoreFields + ).toEqual(['/(.*)/', '/(.valid*)/']); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index a1c6601520a54..0850e43b21eda 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -21,12 +21,61 @@ export const configSchema = schema.object({ maxRuleImportPayloadBytes: schema.number({ defaultValue: 10485760 }), maxTimelineImportExportSize: schema.number({ defaultValue: 10000 }), maxTimelineImportPayloadBytes: schema.number({ defaultValue: 10485760 }), + + /** + * This is used within the merge strategies: + * server/lib/detection_engine/signals/source_fields_merging + * + * For determining which strategy for merging "fields" and "_source" together to get + * runtime fields, constant keywords, etc... + * + * "missingFields" (default) This will only merge fields that are missing from the _source and exist in the fields. + * "noFields" This will turn off all merging of runtime fields, constant keywords from fields. + * "allFields" This will merge and overwrite anything found within "fields" into "_source" before indexing the data. + */ alertMergeStrategy: schema.oneOf( [schema.literal('allFields'), schema.literal('missingFields'), schema.literal('noFields')], { defaultValue: 'missingFields', } ), + + /** + * This is used within the merge strategies: + * server/lib/detection_engine/signals/source_fields_merging + * + * For determining if we need to ignore particular "fields" and not merge them with "_source" such as + * runtime fields, constant keywords, etc... + * + * This feature and functionality is mostly as "safety feature" meaning that we have had bugs in the past + * where something down the stack unexpectedly ends up in the fields API which causes documents to not + * be indexable. Rather than changing alertMergeStrategy to be "noFields", you can use this array to add + * any problematic values. + * + * You can use plain dotted notation strings such as "host.name" or a regular expression such as "/host\..+/" + */ + alertIgnoreFields: schema.arrayOf(schema.string(), { + defaultValue: [], + validate(ignoreFields) { + const errors = ignoreFields.flatMap((ignoreField, index) => { + if (ignoreField.startsWith('/') && ignoreField.endsWith('/')) { + try { + new RegExp(ignoreField.slice(1, -1)); + return []; + } catch (error) { + return [`"${error.message}" at array position ${index}`]; + } + } else { + return []; + } + }); + if (errors.length !== 0) { + return errors.join('. '); + } else { + return undefined; + } + }, + }), [SIGNALS_INDEX_KEY]: schema.string({ defaultValue: DEFAULT_SIGNALS_INDEX }), /** diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index 83f38bc904576..4bd63c83169e5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -48,19 +48,13 @@ describe('Action Log API', () => { }).not.toThrow(); }); - it('should work without query params', () => { + it('should not work when no params while requesting with query params', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({}); - }).not.toThrow(); - }); - - it('should work with query params', () => { - expect(() => { - EndpointActionLogRequestSchema.query.validate({ page: 10, page_size: 100 }); - }).not.toThrow(); + }).toThrow(); }); - it('should work with all query params', () => { + it('should work with all required query params', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({ page: 10, @@ -71,24 +65,24 @@ describe('Action Log API', () => { }).not.toThrow(); }); - it('should work with just startDate', () => { + it('should not work without endDate', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({ page: 1, page_size: 100, start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday }); - }).not.toThrow(); + }).toThrow(); }); - it('should work with just endDate', () => { + it('should not work without startDate', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({ page: 1, page_size: 100, end_date: new Date().toISOString(), // today }); - }).not.toThrow(); + }).toThrow(); }); it('should not work without allowed page and page_size params', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index 80fb1c5d9c7b0..a04a6eea5ab65 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -31,8 +31,8 @@ export const getAuditLogResponse = async ({ elasticAgentId: string; page: number; pageSize: number; - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; context: SecuritySolutionRequestHandlerContext; logger: Logger; }): Promise => { @@ -71,8 +71,8 @@ const getActivityLog = async ({ elasticAgentId: string; size: number; from: number; - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; logger: Logger; }) => { const options = { @@ -84,13 +84,10 @@ const getActivityLog = async ({ let actionsResult; let responsesResult; - const dateFilters = []; - if (startDate) { - dateFilters.push({ range: { '@timestamp': { gte: startDate } } }); - } - if (endDate) { - dateFilters.push({ range: { '@timestamp': { lte: endDate } } }); - } + const dateFilters = [ + { range: { '@timestamp': { gte: startDate } } }, + { range: { '@timestamp': { lte: endDate } } }, + ]; try { // fetch actions with matching agent_id diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts index a768273c9d147..1ac85f9a27969 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts @@ -26,6 +26,7 @@ export const createMockConfig = (): ConfigType => ({ endpointResultListDefaultPageSize: 10, packagerTaskInterval: '60s', alertMergeStrategy: 'missingFields', + alertIgnoreFields: [], prebuiltRulesFromFileSystem: true, prebuiltRulesFromSavedObjects: false, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts index f0da8dad16ab0..a5515f8db8552 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts @@ -142,76 +142,78 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { invariant(result.aggregations, 'Search response should contain aggregations'); return Object.fromEntries( - result.aggregations.rules.buckets.map((bucket) => [ - bucket.key, - bucket.most_recent_logs.hits.hits.map((event) => { - const logEntry = parseRuleExecutionLog(event._source); - invariant( - logEntry[ALERT_RULE_UUID] ?? '', - 'Malformed execution log entry: rule.id field not found' - ); + result.aggregations.rules.buckets.map<[ruleId: string, logs: IRuleStatusSOAttributes[]]>( + (bucket) => [ + bucket.key as string, + bucket.most_recent_logs.hits.hits.map((event) => { + const logEntry = parseRuleExecutionLog(event._source); + invariant( + logEntry[ALERT_RULE_UUID] ?? '', + 'Malformed execution log entry: rule.id field not found' + ); - const lastFailure = bucket.last_failure.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source) - : undefined; + const lastFailure = bucket.last_failure.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source) + : undefined; - const lastSuccess = bucket.last_success.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.last_success.event.hits.hits[0]._source) - : undefined; + const lastSuccess = bucket.last_success.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.last_success.event.hits.hits[0]._source) + : undefined; - const lookBack = bucket.indexing_lookback.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.indexing_lookback.event.hits.hits[0]._source) - : undefined; + const lookBack = bucket.indexing_lookback.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.indexing_lookback.event.hits.hits[0]._source) + : undefined; - const executionGap = bucket.execution_gap.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.execution_gap.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.executionGap) - ] - : undefined; + const executionGap = bucket.execution_gap.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.execution_gap.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.executionGap) + ] + : undefined; - const searchDuration = bucket.search_duration_max.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.search_duration_max.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.searchDurationMax) - ] - : undefined; + const searchDuration = bucket.search_duration_max.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.search_duration_max.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.searchDurationMax) + ] + : undefined; - const indexingDuration = bucket.indexing_duration_max.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.indexing_duration_max.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.indexingDurationMax) - ] - : undefined; + const indexingDuration = bucket.indexing_duration_max.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.indexing_duration_max.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.indexingDurationMax) + ] + : undefined; - const alertId = logEntry[ALERT_RULE_UUID] ?? ''; - const statusDate = logEntry[TIMESTAMP]; - const lastFailureAt = lastFailure?.[TIMESTAMP]; - const lastFailureMessage = lastFailure?.[MESSAGE]; - const lastSuccessAt = lastSuccess?.[TIMESTAMP]; - const lastSuccessMessage = lastSuccess?.[MESSAGE]; - const status = (logEntry[RULE_STATUS] as RuleExecutionStatus) || null; - const lastLookBackDate = lookBack?.[getMetricField(ExecutionMetric.indexingLookback)]; - const gap = executionGap ? moment.duration(executionGap).humanize() : null; - const bulkCreateTimeDurations = indexingDuration - ? [makeFloatString(indexingDuration)] - : null; - const searchAfterTimeDurations = searchDuration - ? [makeFloatString(searchDuration)] - : null; + const alertId = logEntry[ALERT_RULE_UUID] ?? ''; + const statusDate = logEntry[TIMESTAMP]; + const lastFailureAt = lastFailure?.[TIMESTAMP]; + const lastFailureMessage = lastFailure?.[MESSAGE]; + const lastSuccessAt = lastSuccess?.[TIMESTAMP]; + const lastSuccessMessage = lastSuccess?.[MESSAGE]; + const status = (logEntry[RULE_STATUS] as RuleExecutionStatus) || null; + const lastLookBackDate = lookBack?.[getMetricField(ExecutionMetric.indexingLookback)]; + const gap = executionGap ? moment.duration(executionGap).humanize() : null; + const bulkCreateTimeDurations = indexingDuration + ? [makeFloatString(indexingDuration)] + : null; + const searchAfterTimeDurations = searchDuration + ? [makeFloatString(searchDuration)] + : null; - return { - alertId, - statusDate, - lastFailureAt, - lastFailureMessage, - lastSuccessAt, - lastSuccessMessage, - status, - lastLookBackDate, - gap, - bulkCreateTimeDurations, - searchAfterTimeDurations, - }; - }), - ]) + return { + alertId, + statusDate, + lastFailureAt, + lastFailureMessage, + lastSuccessAt, + lastSuccessMessage, + status, + lastLookBackDate, + gap, + bulkCreateTimeDurations, + searchAfterTimeDurations, + }; + }), + ] + ) ); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts index 879d776f83df2..3992c3afaa302 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts @@ -40,6 +40,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, ruleDataService, }) => (type) => { @@ -208,6 +209,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ const wrapHits = wrapHitsFactory({ logger, + ignoreFields, mergeStrategy, ruleSO, spaceId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts index ae2ebc787451b..c09d707fe484e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts @@ -36,10 +36,11 @@ export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, mergeStrategy: ConfigType['alertMergeStrategy'], + ignoreFields: ConfigType['alertIgnoreFields'], applyOverrides: boolean, buildReasonMessage: BuildReasonMessage ): RACAlert => { - const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); + const mergedDoc = getMergeStrategy(mergeStrategy)({ doc, ignoreFields }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}) : buildRuleWithoutOverrides(ruleSO); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts index 62946c52b7f40..95c7b4e90b29c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts @@ -7,7 +7,7 @@ import { Logger } from 'kibana/server'; -import { SearchAfterAndBulkCreateParams, SignalSourceHit, WrapHits } from '../../signals/types'; +import { SearchAfterAndBulkCreateParams, WrapHits } from '../../signals/types'; import { buildBulkBody } from './utils/build_bulk_body'; import { generateId } from '../../signals/utils'; import { filterDuplicateSignals } from '../../signals/filter_duplicate_signals'; @@ -16,6 +16,7 @@ import { WrappedRACAlert } from '../types'; export const wrapHitsFactory = ({ logger, + ignoreFields, mergeStrategy, ruleSO, spaceId, @@ -23,6 +24,7 @@ export const wrapHitsFactory = ({ logger: Logger; ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; spaceId: string | null | undefined; }): WrapHits => (events, buildReasonMessage) => { try { @@ -38,8 +40,9 @@ export const wrapHitsFactory = ({ _source: buildBulkBody( spaceId, ruleSO, - doc as SignalSourceHit, + doc, mergeStrategy, + ignoreFields, true, buildReasonMessage ), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts index f13a5a5e0e715..fe836c872dcad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts @@ -56,6 +56,7 @@ describe('Indicator Match Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, + ignoreFields: [], mergeStrategy: 'allFields', ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, @@ -97,6 +98,7 @@ describe('Indicator Match Alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, version: '1.0.0', @@ -135,6 +137,7 @@ describe('Indicator Match Alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, version: '1.0.0', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts index 71acc2e1cee85..f2dfe69debed0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts @@ -19,6 +19,7 @@ export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions) lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, version, ruleDataService, @@ -27,6 +28,7 @@ export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions) lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, ruleDataService, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts index 40566ffa04e6a..23cd2e94aedf8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts @@ -98,6 +98,7 @@ describe('Machine Learning Alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ml: mlMock, ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts index 1d872df35de3a..cdaeb4be76d02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -14,11 +14,20 @@ import { createSecurityRuleTypeFactory } from '../create_security_rule_type_fact import { CreateRuleOptions } from '../types'; export const createMlAlertType = (createOptions: CreateRuleOptions) => { - const { lists, logger, mergeStrategy, ml, ruleDataClient, ruleDataService } = createOptions; + const { + lists, + logger, + mergeStrategy, + ignoreFields, + ml, + ruleDataClient, + ruleDataService, + } = createOptions; const createSecurityRuleType = createSecurityRuleTypeFactory({ lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, ruleDataService, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index 903cf6adadd43..ed791af08890c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -32,6 +32,7 @@ describe('Custom query alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, version: '1.0.0', @@ -79,6 +80,7 @@ describe('Custom query alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, version: '1.0.0', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts index e59037f38ce56..2f185853754b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts @@ -19,6 +19,7 @@ export const createQueryAlertType = (createOptions: CreateRuleOptions) => { lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, version, ruleDataService, @@ -27,6 +28,7 @@ export const createQueryAlertType = (createOptions: CreateRuleOptions) => { lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, ruleDataService, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index f061240c4a6e5..d50ab566c75cb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -96,6 +96,7 @@ export type CreateSecurityRuleTypeFactory = (options: { lists: SetupPlugins['lists']; logger: Logger; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; ruleDataClient: IRuleDataClient; ruleDataService: IRuleDataPluginService; }) => < @@ -124,6 +125,7 @@ export interface CreateRuleOptions { lists: SetupPlugins['lists']; logger: Logger; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; ml?: SetupPlugins['ml']; ruleDataClient: IRuleDataClient; version: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 206f3ae59d246..5f392bed75f76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -43,6 +43,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -114,6 +115,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -199,6 +201,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -270,6 +273,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -338,6 +342,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -405,6 +410,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); const expected: Omit & { someKey: string } = { @@ -468,6 +474,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); const expected: Omit & { someKey: string } = { @@ -712,6 +719,7 @@ describe('buildSignalFromEvent', () => { ruleSO, true, 'missingFields', + [], buildReasonMessage ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index a4e812e8f111a..f8e39964523d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -37,9 +37,10 @@ export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, mergeStrategy: ConfigType['alertMergeStrategy'], + ignoreFields: ConfigType['alertIgnoreFields'], buildReasonMessage: BuildReasonMessage ): SignalHit => { - const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); + const mergedDoc = getMergeStrategy(mergeStrategy)({ doc, ignoreFields }); const rule = buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}); const timestamp = new Date().toISOString(); const reason = buildReasonMessage({ mergedDoc, rule }); @@ -76,11 +77,19 @@ export const buildSignalGroupFromSequence = ( ruleSO: SavedObject, outputIndex: string, mergeStrategy: ConfigType['alertMergeStrategy'], + ignoreFields: ConfigType['alertIgnoreFields'], buildReasonMessage: BuildReasonMessage ): WrappedSignalHit[] => { const wrappedBuildingBlocks = wrapBuildingBlocks( sequence.events.map((event) => { - const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy, buildReasonMessage); + const signal = buildSignalFromEvent( + event, + ruleSO, + false, + mergeStrategy, + ignoreFields, + buildReasonMessage + ); signal.signal.rule.building_block_type = 'default'; return signal; }), @@ -146,9 +155,10 @@ export const buildSignalFromEvent = ( ruleSO: SavedObject, applyOverrides: boolean, mergeStrategy: ConfigType['alertMergeStrategy'], + ignoreFields: ConfigType['alertIgnoreFields'], buildReasonMessage: BuildReasonMessage ): SignalHit => { - const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event }); + const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event, ignoreFields }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedEvent._source ?? {}) : buildRuleWithoutOverrides(ruleSO); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 8bf0c986b9c25..55a184a1c0bcc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -74,6 +74,7 @@ describe('searchAfterAndBulkCreate', () => { ruleSO, signalsIndex: DEFAULT_SIGNALS_INDEX, mergeStrategy: 'missingFields', + ignoreFields: [], }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 39728235db39c..9af8680ec726a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -195,6 +195,7 @@ describe('signal_rule_alert_type', () => { ml: mlMock, lists: listMock.createSetup(), mergeStrategy: 'missingFields', + ignoreFields: [], ruleDataService, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 1c4efea0a1d59..68d60f7757e4a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -82,6 +82,7 @@ export const signalRulesAlertType = ({ ml, lists, mergeStrategy, + ignoreFields, ruleDataService, }: { logger: Logger; @@ -91,6 +92,7 @@ export const signalRulesAlertType = ({ ml: SetupPlugins['ml']; lists: SetupPlugins['lists'] | undefined; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; ruleDataService: IRuleDataPluginService; }): SignalRuleAlertTypeDefinition => { return { @@ -275,12 +277,14 @@ export const signalRulesAlertType = ({ ruleSO: savedObject, signalsIndex: params.outputIndex, mergeStrategy, + ignoreFields, }); const wrapSequences = wrapSequencesFactory({ ruleSO: savedObject, signalsIndex: params.outputIndex, mergeStrategy, + ignoreFields, }); if (isMlRule(type)) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.test.ts index b900ea268fd6e..6af82d3a71028 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.test.ts @@ -44,7 +44,7 @@ describe('merge_all_fields_with_source', () => { test('when source is "undefined", merged doc is "undefined"', () => { const _source: SignalSourceHit['_source'] = {}; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -53,7 +53,7 @@ describe('merge_all_fields_with_source', () => { foo: [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -62,7 +62,7 @@ describe('merge_all_fields_with_source', () => { foo: 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -71,7 +71,7 @@ describe('merge_all_fields_with_source', () => { foo: ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -80,7 +80,7 @@ describe('merge_all_fields_with_source', () => { foo: ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -89,7 +89,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -98,7 +98,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -107,7 +107,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -133,7 +133,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -142,7 +142,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -151,7 +151,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -160,7 +160,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -169,7 +169,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -178,7 +178,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -187,7 +187,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -217,7 +217,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: [] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -226,7 +226,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: 'value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -235,7 +235,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: ['value'] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -244,7 +244,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: ['value_1', 'value_2'] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -253,7 +253,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: { mars: 'some value' } }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -262,7 +262,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: [{ mars: 'some value' }] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -271,7 +271,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: [{ mars: 'some value' }, { mars: 'some other value' }] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -299,7 +299,7 @@ describe('merge_all_fields_with_source', () => { 'bar.foo': [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -308,7 +308,7 @@ describe('merge_all_fields_with_source', () => { 'bar.foo': 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -317,7 +317,7 @@ describe('merge_all_fields_with_source', () => { 'bar.foo': ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -326,7 +326,7 @@ describe('merge_all_fields_with_source', () => { 'bar.foo': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -335,7 +335,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -344,7 +344,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -353,7 +353,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -376,7 +376,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'other_value_1', @@ -389,7 +389,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'], @@ -402,7 +402,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: { zed: 'other_value_1' } }, }); @@ -413,7 +413,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -440,7 +440,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'other_value_1', @@ -453,7 +453,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'] }, }); @@ -464,7 +464,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'other_value_1', @@ -477,7 +477,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'other_value_1' }, { bar: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: [{ bar: 'other_value_1' }, { bar: 'other_value_2' }], }); @@ -503,7 +503,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.bar': 'other_value_1', }); @@ -514,7 +514,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -523,7 +523,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.bar': { zed: 'other_value_1' }, }); @@ -534,7 +534,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -560,7 +560,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1'] }, }); @@ -571,7 +571,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'] }, }); @@ -582,7 +582,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }] }, }); @@ -593,7 +593,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -619,7 +619,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -628,7 +628,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -637,7 +637,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -646,7 +646,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -670,7 +670,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1'] }, }); @@ -681,7 +681,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'] }, }); @@ -692,7 +692,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }] }, }); @@ -703,7 +703,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -729,7 +729,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -738,7 +738,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -747,7 +747,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -756,7 +756,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -782,7 +782,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1'], @@ -795,7 +795,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'], @@ -808,7 +808,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }], @@ -821,7 +821,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], @@ -849,7 +849,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -858,7 +858,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -867,7 +867,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -876,7 +876,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -902,7 +902,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -911,7 +911,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -920,7 +920,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: { zed: 'other_value_1' } }, }); @@ -931,7 +931,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -957,7 +957,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -966,7 +966,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -975,7 +975,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.bar': { mars: 'other_value_1' }, }); @@ -986,7 +986,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }); @@ -1014,7 +1014,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1023,7 +1023,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1032,7 +1032,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }] }, }); @@ -1043,7 +1043,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -1069,7 +1069,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1078,7 +1078,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1087,7 +1087,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }] }, }); @@ -1098,7 +1098,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -1124,7 +1124,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1133,7 +1133,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1142,7 +1142,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -1151,7 +1151,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -1175,7 +1175,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1184,7 +1184,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1193,7 +1193,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -1202,7 +1202,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -1228,7 +1228,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'value_1' }, 'foo.bar': 'other_value_1', @@ -1243,7 +1243,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'value_1' }, // <--- We have duplicated value_1 twice which is a bug 'foo.bar': ['value_1', 'value_2'], // <-- We have merged the array value because we do not understand if we should or not @@ -1270,7 +1270,7 @@ describe('merge_all_fields_with_source', () => { 'bar.keyword': ['bar_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'foo_other_value_1', bar: 'bar_other_value_1', @@ -1291,7 +1291,7 @@ describe('merge_all_fields_with_source', () => { 'host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ host: { hostname: 'hostname_other_value_1', @@ -1316,7 +1316,7 @@ describe('merge_all_fields_with_source', () => { 'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { host: { @@ -1334,7 +1334,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'other_value_1', }); @@ -1354,7 +1354,7 @@ describe('merge_all_fields_with_source', () => { 'host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'host.name': 'host_name_other_value_1', 'host.hostname': 'hostname_other_value_1', @@ -1373,7 +1373,7 @@ describe('merge_all_fields_with_source', () => { 'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.host.name': 'host_name_other_value_1', 'foo.host.hostname': 'hostname_other_value_1', @@ -1388,7 +1388,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar.zed': ['zed_other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'other_value_1', }); @@ -1415,7 +1415,7 @@ describe('merge_all_fields_with_source', () => { 'foo.mars': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1435,7 +1435,7 @@ describe('merge_all_fields_with_source', () => { 'foo.zed.mars': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1450,7 +1450,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: ['single_value'], zed: ['single_value'] }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'single_value', zed: 'single_value' }, }); @@ -1469,10 +1469,132 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: ['single_value'], zed: ['single_value'] }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: [{ bar: ['single_value'], zed: ['single_value'] }], }); }); }); + + /** + * Small set of tests to ensure that ignore fields are wired up at the strategy level + */ + describe('ignore fields', () => { + test('Does not merge an ignored field if it does not exist already in the _source', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.ignore': ['other_value_2'], // string value should ignore this + '_odd.value': ['other_value_2'], // Regex should ignore this value of: /[_]+/ + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: ['value.should.ignore', '/[_]+/'], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + }); + }); + + test('Does merge fields when no matching happens', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.work': ['other_value_2'], + '_odd.value': ['other_value_2'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: ['other.string', '/[z]+/'], // Neither of these two should match anything + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + _odd: { + value: 'other_value_2', + }, + value: { + should: { + work: 'other_value_2', + }, + }, + }); + }); + + test('Does not update an ignored field and keeps the original value if it matches in the ignoreFields', () => { + const _source: SignalSourceHit['_source'] = { + 'value.should.ignore': ['value_1'], + '_odd.value': ['value_2'], + }; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.ignore': ['other_value_2'], // string value should ignore this + '_odd.value': ['other_value_2'], // Regex should ignore this value of: /[_]+/ + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: ['value.should.ignore', '/[_]+/'], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + 'value.should.ignore': ['value_1'], + '_odd.value': ['value_2'], + }); + }); + + test('Does not ignore anything when no matching happens and overwrites the expected fields', () => { + const _source: SignalSourceHit['_source'] = { + 'value.should.ignore': ['value_1'], + '_odd.value': ['value_2'], + }; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.ignore': ['other_value_2'], + '_odd.value': ['other_value_2'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: ['nothing.to.match', '/[z]+/'], // these match nothing + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + 'value.should.ignore': ['other_value_2'], + '_odd.value': ['other_value_2'], + }); + }); + }); + + /** + * Test that the EQL bug workaround is wired up. Remove this once the bug is fixed. + */ + describe('Works around EQL bug 77152 (https://github.com/elastic/elasticsearch/issues/77152)', () => { + test('Does not merge field that contains _ignored', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + _ignored: ['other_value_1'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: [], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts index da2eea9d2c61e..ade83b88d526b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts @@ -23,14 +23,16 @@ import { isTypeObject } from '../utils/is_type_object'; * on this function and the general strategies. * * @param doc The document with "_source" and "fields" - * @param throwOnFailSafe Defaults to false, but if set to true it will cause a throw if the fail safe is triggered to indicate we need to add a new explicit test condition + * @param ignoreFields Any fields that we should ignore and never merge from "fields". If the value exists + * within doc._source it will be untouched and used. If the value does not exist within the doc._source, + * it will not be added from fields. * @returns The two merged together in one object where we can */ -export const mergeAllFieldsWithSource: MergeStrategyFunction = ({ doc }) => { +export const mergeAllFieldsWithSource: MergeStrategyFunction = ({ doc, ignoreFields }) => { const source = doc._source ?? {}; const fields = doc.fields ?? {}; const fieldEntries = Object.entries(fields); - const filteredEntries = filterFieldEntries(fieldEntries); + const filteredEntries = filterFieldEntries(fieldEntries, ignoreFields); const transformedSource = filteredEntries.reduce( (merged, [fieldsKey, fieldsValue]: [string, FieldsType]) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.test.ts index 70d1e79580e84..612bff75792da 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.test.ts @@ -44,7 +44,7 @@ describe('merge_missing_fields_with_source', () => { test('when source is "undefined", merged doc is "undefined"', () => { const _source: SignalSourceHit['_source'] = {}; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -53,7 +53,7 @@ describe('merge_missing_fields_with_source', () => { foo: [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -62,7 +62,7 @@ describe('merge_missing_fields_with_source', () => { foo: 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -71,7 +71,7 @@ describe('merge_missing_fields_with_source', () => { foo: ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -80,7 +80,7 @@ describe('merge_missing_fields_with_source', () => { foo: ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -89,7 +89,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -98,7 +98,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -107,7 +107,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -133,7 +133,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -142,7 +142,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -151,7 +151,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -160,7 +160,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -169,7 +169,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -178,7 +178,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -187,7 +187,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -217,7 +217,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: [] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -226,7 +226,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: 'value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -235,7 +235,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: ['value'] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -244,7 +244,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: ['value_1', 'value_2'] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -253,7 +253,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: { mars: 'some value' } }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -262,7 +262,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: [{ mars: 'some value' }] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -271,7 +271,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: [{ mars: 'some value' }, { mars: 'some other value' }] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -299,7 +299,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.foo': [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -308,7 +308,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.foo': 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -317,7 +317,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.foo': ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -326,7 +326,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.foo': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -335,7 +335,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -344,7 +344,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -353,7 +353,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -376,7 +376,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'other_value_1', @@ -389,7 +389,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'], @@ -402,7 +402,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({}); }); @@ -411,7 +411,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({}); }); }); @@ -436,7 +436,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -445,7 +445,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -454,7 +454,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -463,7 +463,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'other_value_1' }, { bar: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -487,7 +487,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -496,7 +496,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -505,7 +505,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -514,7 +514,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -540,7 +540,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -549,7 +549,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -558,7 +558,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -567,7 +567,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -591,7 +591,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -600,7 +600,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -609,7 +609,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -618,7 +618,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -642,7 +642,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -651,7 +651,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -660,7 +660,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -669,7 +669,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -693,7 +693,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -702,7 +702,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -711,7 +711,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -720,7 +720,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -746,7 +746,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -755,7 +755,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -764,7 +764,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -773,7 +773,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -797,7 +797,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -806,7 +806,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -815,7 +815,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -824,7 +824,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -850,7 +850,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -859,7 +859,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -868,7 +868,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -877,7 +877,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -901,7 +901,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -910,7 +910,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -919,7 +919,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -928,7 +928,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -954,7 +954,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -963,7 +963,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -972,7 +972,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -981,7 +981,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1005,7 +1005,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1014,7 +1014,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1023,7 +1023,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1032,7 +1032,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1056,7 +1056,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1065,7 +1065,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1074,7 +1074,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1083,7 +1083,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1107,7 +1107,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1116,7 +1116,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1125,7 +1125,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1134,7 +1134,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1160,7 +1160,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1172,7 +1172,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1196,7 +1196,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.keyword': ['bar_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1214,7 +1214,7 @@ describe('merge_missing_fields_with_source', () => { 'host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1234,7 +1234,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1245,7 +1245,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'other_value_1', }); @@ -1265,7 +1265,7 @@ describe('merge_missing_fields_with_source', () => { 'host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1281,7 +1281,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1293,7 +1293,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar.zed': ['zed_other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'other_value_1', }); @@ -1320,7 +1320,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.mars': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1340,7 +1340,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.zed.mars': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1355,7 +1355,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: ['single_value'], zed: ['single_value'] }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({}); }); @@ -1372,8 +1372,82 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: ['single_value'], zed: ['single_value'] }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); + + /** + * Small set of tests to ensure that ignore fields are wired up at the strategy level + */ + describe('ignore fields', () => { + test('Does not merge an ignored field if it does not exist already in the _source', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.ignore': ['other_value_2'], // string value should ignore this + '_odd.value': ['other_value_2'], // Regex should ignore this value of: /[_]+/ + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeMissingFieldsWithSource({ + doc, + ignoreFields: ['value.should.ignore', '/[_]+/'], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + }); + }); + + test('Does merge fields when no matching happens', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.work': ['other_value_2'], + '_odd.value': ['other_value_2'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeMissingFieldsWithSource({ + doc, + ignoreFields: ['other.string', '/[z]+/'], // Neither of these two should match anything + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + _odd: { + value: 'other_value_2', + }, + value: { + should: { + work: 'other_value_2', + }, + }, + }); + }); + }); + + /** + * Test that the EQL bug workaround is wired up. Remove this once the bug is fixed. + */ + describe('Works around EQL bug 77152 (https://github.com/elastic/elasticsearch/issues/77152)', () => { + test('Does not merge field that contains _ignored', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + _ignored: ['other_value_1'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeMissingFieldsWithSource({ + doc, + ignoreFields: [], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts index b66c46ccbf0ca..611a3ad879705 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts @@ -19,14 +19,16 @@ import { isNestedObject } from '../utils/is_nested_object'; * Merges only missing sections of "doc._source" with its "doc.fields" on a "best effort" basis. See ../README.md for more information * on this function and the general strategies. * @param doc The document with "_source" and "fields" - * @param throwOnFailSafe Defaults to false, but if set to true it will cause a throw if the fail safe is triggered to indicate we need to add a new explicit test condition + * @param ignoreFields Any fields that we should ignore and never merge from "fields". If the value exists + * within doc._source it will be untouched and used. If the value does not exist within the doc._source, + * it will not be added from fields. * @returns The two merged together in one object where we can */ -export const mergeMissingFieldsWithSource: MergeStrategyFunction = ({ doc }) => { +export const mergeMissingFieldsWithSource: MergeStrategyFunction = ({ doc, ignoreFields }) => { const source = doc._source ?? {}; const fields = doc.fields ?? {}; const fieldEntries = Object.entries(fields); - const filteredEntries = filterFieldEntries(fieldEntries); + const filteredEntries = filterFieldEntries(fieldEntries, ignoreFields); const transformedSource = filteredEntries.reduce( (merged, [fieldsKey, fieldsValue]: [string, FieldsType]) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts index 6c2daf2526715..5e26b619fbdfa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts @@ -10,6 +10,7 @@ import { MergeStrategyFunction } from '../types'; /** * Does nothing and does not merge source with fields * @param doc The doc to return and do nothing + * @param ignoreFields We do nothing with this value and ignore it if set * @returns The doc as a no operation and do nothing */ -export const mergeNoFields: MergeStrategyFunction = ({ doc }) => doc; +export const mergeNoFields: MergeStrategyFunction = ({ doc, ignoreFields }) => doc; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts index 1438d2844949c..0b847064d5d62 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts @@ -14,5 +14,13 @@ export type FieldsType = string[] | number[] | boolean[] | object[]; /** * The type of the merge strategy functions which must implement to be part of the strategy group + * @param doc The document to send in to merge + * @param ignoreFields Fields you want to ignore and not merge. */ -export type MergeStrategyFunction = ({ doc }: { doc: SignalSourceHit }) => SignalSourceHit; +export type MergeStrategyFunction = ({ + doc, + ignoreFields, +}: { + doc: SignalSourceHit; + ignoreFields: string[]; +}) => SignalSourceHit; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.test.ts index 9cc2478290885..031a2013b462e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.test.ts @@ -27,7 +27,9 @@ describe('filter_field_entries', () => { test('returns a single valid fieldEntries as expected', () => { const fieldEntries: Array<[string, FieldsType]> = [['foo.bar', dummyValue]]; - expect(filterFieldEntries(fieldEntries)).toEqual(fieldEntries); + expect(filterFieldEntries(fieldEntries, [])).toEqual( + fieldEntries + ); }); test('removes invalid dotted entries', () => { @@ -37,7 +39,7 @@ describe('filter_field_entries', () => { ['..', dummyValue], ['foo..bar', dummyValue], ]; - expect(filterFieldEntries(fieldEntries)).toEqual([ + expect(filterFieldEntries(fieldEntries, [])).toEqual([ ['foo.bar', dummyValue], ]); }); @@ -49,7 +51,7 @@ describe('filter_field_entries', () => { ['bar.keyword', dummyValue], // <-- "bar.keyword" multi-field should be removed ['bar', dummyValue], ]; - expect(filterFieldEntries(fieldEntries)).toEqual([ + expect(filterFieldEntries(fieldEntries, [])).toEqual([ ['foo', dummyValue], ['bar', dummyValue], ]); @@ -62,7 +64,7 @@ describe('filter_field_entries', () => { ['host.hostname', dummyValue], ['host.hostname.keyword', dummyValue], // <-- multi-field should be removed ]; - expect(filterFieldEntries(fieldEntries)).toEqual([ + expect(filterFieldEntries(fieldEntries, [])).toEqual([ ['host.name', dummyValue], ['host.hostname', dummyValue], ]); @@ -75,9 +77,34 @@ describe('filter_field_entries', () => { ['foo.host.hostname', dummyValue], ['foo.host.hostname.keyword', dummyValue], // <-- multi-field should be removed ]; - expect(filterFieldEntries(fieldEntries)).toEqual([ + expect(filterFieldEntries(fieldEntries, [])).toEqual([ ['foo.host.name', dummyValue], ['foo.host.hostname', dummyValue], ]); }); + + test('ignores fields of "_ignore", for eql bug https://github.com/elastic/elasticsearch/issues/77152', () => { + const fieldEntries: Array<[string, FieldsType]> = [ + ['_ignored', dummyValue], + ['foo.host.hostname', dummyValue], + ]; + expect(filterFieldEntries(fieldEntries, [])).toEqual([ + ['foo.host.hostname', dummyValue], + ]); + }); + + test('ignores fields given strings and regular expressions in the ignoreFields list', () => { + const fieldEntries: Array<[string, FieldsType]> = [ + ['host.name', dummyValue], + ['user.name', dummyValue], // <-- string from ignoreFields should ignore this + ['host.hostname', dummyValue], + ['_odd.value', dummyValue], // <-- regular expression from ignoreFields should ignore this + ]; + expect( + filterFieldEntries(fieldEntries, ['user.name', '/[_]+/']) + ).toEqual([ + ['host.name', dummyValue], + ['host.hostname', dummyValue], + ]); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.ts index 221cdabc62847..4ee5fa1db52f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.ts @@ -9,6 +9,8 @@ import { isMultiField } from './is_multifield'; import { isInvalidKey } from './is_invalid_key'; import { isTypeObject } from './is_type_object'; import { FieldsType } from '../types'; +import { isIgnored } from './is_ignored'; +import { isEqlBug77152 } from './is_eql_bug_77152'; /** * Filters field entries by removing invalid field entries such as any invalid characters @@ -17,13 +19,18 @@ import { FieldsType } from '../types'; * those and don't try to merge those. * * @param fieldEntries The field entries to filter + * @param ignoreFields Array of fields to ignore. If a value starts and ends with "/", such as: "/[_]+/" then the field will be treated as a regular expression. + * If you have an object structure to ignore such as "{ a: { b: c: {} } } ", then you need to ignore it as the string "a.b.c" * @returns The field entries filtered */ export const filterFieldEntries = ( - fieldEntries: Array<[string, FieldsType]> + fieldEntries: Array<[string, FieldsType]>, + ignoreFields: string[] ): Array<[string, FieldsType]> => { return fieldEntries.filter(([fieldsKey, fieldsValue]: [string, FieldsType]) => { return ( + !isEqlBug77152(fieldsKey) && + !isIgnored(fieldsKey, ignoreFields) && !isInvalidKey(fieldsKey) && !isMultiField(fieldsKey, fieldEntries) && !isTypeObject(fieldsValue) // TODO: Look at not filtering this and instead transform it so it can be inserted correctly in the strategies which does an overwrite of everything from fields diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/index.ts index baf9efca511e2..87b1097dd9bca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/index.ts @@ -7,6 +7,7 @@ export * from './array_in_path_exists'; export * from './filter_field_entries'; export * from './is_array_of_primitives'; +export * from './is_ignored'; export * from './is_invalid_key'; export * from './is_multifield'; export * from './is_nested_object'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.test.ts new file mode 100644 index 0000000000000..47a56e096649b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEqlBug77152 } from './is_eql_bug_77152'; + +/** + * @deprecated Remove this test once https://github.com/elastic/elasticsearch/issues/77152 is fixed. + */ +describe('is_eql_bug_77152', () => { + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('returns true if it encounters the bug which is _ignored is returned in the fields', () => { + expect(isEqlBug77152('_ignored')).toEqual(true); + }); + + it('returns false if it encounters a normal field', () => { + expect(isEqlBug77152('some.field')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts new file mode 100644 index 0000000000000..e9a642cd5c382 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Ignores any field that is "_ignored". Customers are not allowed to have this field and more importantly this shows up as a bug + * from EQL as seen here: https://github.com/elastic/elasticsearch/issues/77152 + * Once this ticket is fixed, please remove this function. + * @param fieldsKey The fields key to match against "_ignored" + * @returns true if it is a "_ignored", otherwise false + * @deprecated Remove this once https://github.com/elastic/elasticsearch/issues/77152 is fixed. + */ +export const isEqlBug77152 = (fieldsKey: string): boolean => { + return fieldsKey === '_ignored'; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.test.ts new file mode 100644 index 0000000000000..e4a7093ef127c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { isIgnored } from './is_ignored'; + +describe('is_ignored', () => { + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('string matching', () => { + test('it returns false if given an empty array', () => { + expect(isIgnored('simple.value', [])).toEqual(false); + }); + + test('it returns true if a simple string value matches', () => { + expect(isIgnored('simple.value', ['simple.value'])).toEqual(true); + }); + + test('it returns false if a simple string value does not match', () => { + expect(isIgnored('simple', ['simple.value'])).toEqual(false); + }); + + test('it returns true if a simple string value matches with two strings', () => { + expect(isIgnored('simple.value', ['simple.value', 'simple.second.value'])).toEqual(true); + }); + + test('it returns true if a simple string value matches the second string', () => { + expect(isIgnored('simple.second.value', ['simple.value', 'simple.second.value'])).toEqual( + true + ); + }); + + test('it returns false if a simple string value does not match two strings', () => { + expect(isIgnored('simple', ['simple.value', 'simple.second.value'])).toEqual(false); + }); + + test('it returns true if mixed with a regular expression in the list', () => { + expect(isIgnored('simple', ['simple', '/[_]+/'])).toEqual(true); + }); + }); + + describe('regular expression matching', () => { + test('it returns true if a simple regular expression matches', () => { + expect(isIgnored('_ignored', ['/[_]+/'])).toEqual(true); + }); + + test('it returns false if a simple regular expression does not match', () => { + expect(isIgnored('simple', ['/[_]+/'])).toEqual(false); + }); + + test('it returns true if a simple regular expression matches a longer string', () => { + expect(isIgnored('___ignored', ['/[_]+/'])).toEqual(true); + }); + + test('it returns true if mixed with regular stings', () => { + expect(isIgnored('___ignored', ['simple', '/[_]+/'])).toEqual(true); + }); + + test('it returns true with start anchor', () => { + expect(isIgnored('_ignored', ['simple', '/^[_]+/'])).toEqual(true); + }); + + test('it returns false with start anchor', () => { + expect(isIgnored('simple.something_', ['simple', '/^[_]+/'])).toEqual(false); + }); + + test('it returns true with end anchor', () => { + expect(isIgnored('something_', ['simple', '/[_]+$/'])).toEqual(true); + }); + + test('it returns false with end anchor', () => { + expect(isIgnored('_something', ['simple', '/[_]+$/'])).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts new file mode 100644 index 0000000000000..a418ce735626d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Matches against anything you want to ignore and if it matches that field is ignored. + * @param fieldsKey The fields key to match against + * @param ignoreFields Array of fields to ignore. If a value starts and ends with "/", such as: "/[_]+/" then the field will be treated as a regular expression. + * If you have an object structure to ignore such as "{ a: { b: c: {} } } ", then you need to ignore it as the string "a.b.c" + * @returns true if it is a field to ignore, otherwise false + */ +export const isIgnored = (fieldsKey: string, ignoreFields: string[]): boolean => { + return ignoreFields.some((ignoreField) => { + if (ignoreField.startsWith('/') && ignoreField.endsWith('/')) { + return new RegExp(ignoreField.slice(1, -1)).test(fieldsKey); + } else { + return fieldsKey === ignoreField; + } + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts index 19bdd58140a33..27220d80ebd6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - SearchAfterAndBulkCreateParams, - SignalSourceHit, - WrapHits, - WrappedSignalHit, -} from './types'; +import { SearchAfterAndBulkCreateParams, WrapHits, WrappedSignalHit } from './types'; import { generateId } from './utils'; import { buildBulkBody } from './build_bulk_body'; import { filterDuplicateSignals } from './filter_duplicate_signals'; @@ -20,10 +15,12 @@ export const wrapHitsFactory = ({ ruleSO, signalsIndex, mergeStrategy, + ignoreFields, }: { ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; }): WrapHits => (events, buildReasonMessage) => { const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [ { @@ -34,7 +31,7 @@ export const wrapHitsFactory = ({ String(doc._version), ruleSO.attributes.params.ruleId ?? '' ), - _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy, buildReasonMessage), + _source: buildBulkBody(ruleSO, doc, mergeStrategy, ignoreFields, buildReasonMessage), }, ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts index 0ca4b9688f971..d4a4c55db1d23 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts @@ -13,10 +13,12 @@ export const wrapSequencesFactory = ({ ruleSO, signalsIndex, mergeStrategy, + ignoreFields, }: { ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; }): WrapSequences => (sequences, buildReasonMessage) => sequences.reduce( (acc: WrappedSignalHit[], sequence) => [ @@ -26,6 +28,7 @@ export const wrapSequencesFactory = ({ ruleSO, signalsIndex, mergeStrategy, + ignoreFields, buildReasonMessage ), ], diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index d657d7e06b1a6..4e4d0be5a7411 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -252,6 +252,7 @@ export class Plugin implements IPlugin \ No newline at end of file diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index c3c83f6be72c8..cdfca4e09eb10 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -8,19 +8,12 @@ import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; // @ts-expect-error import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiLoadingContent, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { Direction, EntityType } from '../../../../common/search_strategy'; import type { DocValueFields } from '../../../../common/search_strategy'; @@ -53,6 +46,7 @@ import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } import { Sort } from '../body/sort'; import { InspectButton, InspectButtonContainer } from '../../inspect'; import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector'; +import { TGridLoading, TGridEmpty } from '../shared'; const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; @@ -269,6 +263,8 @@ const TGridIntegratedComponent: React.FC = ({ [deletedEventIds.length, totalCount] ); + const hasAlerts = totalCountMinusDeleted > 0; + const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [ deletedEventIds, events, @@ -300,7 +296,7 @@ const TGridIntegratedComponent: React.FC = ({ data-test-subj="events-viewer-panel" $isFullScreen={globalFullScreen} > - {isFirstUpdate.current && } + {isFirstUpdate.current && } {graphOverlay} @@ -325,61 +321,43 @@ const TGridIntegratedComponent: React.FC = ({ {!graphEventId && graphOverlay == null && ( - - - {totalCountMinusDeleted === 0 && loading === false && ( - - -

- } - titleSize="s" - body={ -

- -

- } - /> - )} - {totalCountMinusDeleted > 0 && ( - - )} - - + <> + {!hasAlerts && !loading && } + {hasAlerts && ( + + + + + + )} + )} )} diff --git a/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx new file mode 100644 index 0000000000000..563e8224058c0 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/shared/index.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 React from 'react'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiImage, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import type { CoreStart } from '../../../../../../../src/core/public'; + +const heights = { + tall: 490, + short: 250, +}; + +export const TGridLoading: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => { + return ( + + + + + + + + ); +}; + +const panelStyle = { + maxWidth: 500, +}; + +export const TGridEmpty: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => { + const { http } = useKibana().services; + + return ( + + + + + + + + +

+ +

+
+

+ +

+
+
+ + + +
+
+
+
+
+ ); +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index ee9b7be48df63..74dd8c01295be 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState, useRef } from 'react'; import styled from 'styled-components'; @@ -39,10 +38,16 @@ import type { State } from '../../../store/t_grid'; import { useTimelineEvents } from '../../../container'; import { StatefulBody } from '../body'; import { LastUpdatedAt } from '../..'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem, UpdatedFlexGroup } from '../styles'; +import { + SELECTOR_TIMELINE_GLOBAL_CONTAINER, + UpdatedFlexItem, + UpdatedFlexGroup, + FullWidthFlexGroup, +} from '../styles'; import { InspectButton, InspectButtonContainer } from '../../inspect'; import { useFetchIndex } from '../../../container/source'; import { AddToCaseAction } from '../../actions/timeline/cases/add_to_case_action'; +import { TGridLoading, TGridEmpty } from '../shared'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const STANDALONE_ID = 'standalone-t-grid'; @@ -68,12 +73,6 @@ const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ flex-direction: column; `; -const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` - overflow: hidden; - margin: 0; - display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; -`; - const ScrollableFlexItem = styled(EuiFlexItem)` overflow: auto; `; @@ -255,6 +254,8 @@ const TGridStandaloneComponent: React.FC = ({ () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), [deletedEventIds.length, totalCount] ); + const hasAlerts = totalCountMinusDeleted > 0; + const activeCaseFlowId = useSelector((state: State) => tGridSelectors.activeCaseFlowId(state)); const selectedEvent = useMemo(() => { const matchedEvent = events.find((event) => event.ecs._id === activeCaseFlowId); @@ -338,14 +339,14 @@ const TGridStandaloneComponent: React.FC = ({ return ( - {isFirstUpdate.current && } + {isFirstUpdate.current && } {canQueryTimeline ? ( <> - + @@ -354,28 +355,9 @@ const TGridStandaloneComponent: React.FC = ({ - {totalCountMinusDeleted === 0 && loading === false && ( - - -

- } - titleSize="s" - body={ -

- -

- } - /> - )} - {totalCountMinusDeleted > 0 && ( + {!hasAlerts && !loading && } + + {hasAlerts && ( ( }) )<{ $isVisible: boolean }>``; +export const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible?: boolean }>` + overflow: hidden; + margin: 0; + min-height: 490px; + display: ${({ $visible = true }) => ($visible ? 'flex' : 'none')}; +`; + export const UpdatedFlexGroup = styled(EuiFlexGroup)` position: absolute; z-index: ${({ theme }) => theme.eui.euiZLevel1}; diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx index 91802c4eb10e1..06bb1ae443216 100644 --- a/x-pack/plugins/timelines/public/methods/index.tsx +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -6,7 +6,7 @@ */ import React, { lazy, Suspense } from 'react'; -import { EuiLoadingContent, EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; import type { Store } from 'redux'; import { Provider } from 'react-redux'; @@ -17,6 +17,7 @@ import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from '. import type { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action'; import { initialTGridState } from '../store/t_grid/reducer'; import { createStore } from '../store/t_grid'; +import { TGridLoading } from '../components/t_grid/shared'; const initializeStore = ({ store, @@ -51,13 +52,7 @@ export const getTGridLazy = ( ) => { initializeStore({ store, storage, setStore }); return ( - - - - } - > + }> ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8827ca008ccd2..87995c9cdd546 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20830,7 +20830,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.mlJobStoppedDescription": "停止", "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAggregatedByDescription": "結果集約条件", "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription": "すべての結果", - "xpack.securitySolution.detectionEngine.ruleDetails.activatedRuleLabel": "有効化", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "削除されたルール", "xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle": "ルール失敗", "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index db902a2c285f6..a017741fccb62 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21299,7 +21299,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.mlJobStoppedDescription": "已停止", "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAggregatedByDescription": "结果聚合依据", "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription": "所有结果", - "xpack.securitySolution.detectionEngine.ruleDetails.activatedRuleLabel": "已激活", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "已删除规则", "xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle": "规则错误位置", "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 4033889d9811e..b72a7fe96817d 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -7,7 +7,7 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["alerting", "features", "home", "spaces"], + "optionalPlugins": ["alerting", "cloud", "features", "home", "spaces"], "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 9786f5dcb949d..ac0e6d95393b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -37,6 +37,7 @@ export interface TriggersAndActionsUiServices extends CoreStart { alerting?: AlertingStart; spaces?: SpacesPluginStart; storage?: Storage; + isCloud: boolean; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; actionTypeRegistry: ActionTypeRegistryContract; ruleTypeRegistry: RuleTypeRegistryContract; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/api.ts new file mode 100644 index 0000000000000..82c787426a38e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/api.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 { HttpSetup } from 'kibana/public'; +import { INTERNAL_BASE_ACTION_API_PATH } from '../../../constants'; +import { EmailConfig } from '../types'; + +export async function getServiceConfig({ + http, + service, +}: { + http: HttpSetup; + service: string; +}): Promise>> { + return await http.get(`${INTERNAL_BASE_ACTION_API_PATH}/connector/_email_config/${service}`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index 4d669ab4c76a1..0e1bf9ef53e15 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -9,6 +9,7 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '../index'; import { ActionTypeModel } from '../../../../types'; import { EmailActionConnector } from '../types'; +import { getEmailServices } from './email'; const ACTION_TYPE_ID = '.email'; let actionTypeModel: ActionTypeModel; @@ -29,6 +30,18 @@ describe('actionTypeRegistry.get() works', () => { }); }); +describe('getEmailServices', () => { + test('should return elastic cloud service if isCloudEnabled is true', () => { + const services = getEmailServices(true); + expect(services.find((service) => service.value === 'elastic_cloud')).toBeTruthy(); + }); + + test('should not return elastic cloud service if isCloudEnabled is false', () => { + const services = getEmailServices(false); + expect(services.find((service) => service.value === 'elastic_cloud')).toBeFalsy(); + }); +}); + describe('connector validation', () => { test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { @@ -46,6 +59,7 @@ describe('connector validation', () => { host: 'localhost', test: 'test', hasAuth: true, + service: 'other', }, } as EmailActionConnector; @@ -55,6 +69,7 @@ describe('connector validation', () => { from: [], port: [], host: [], + service: [], }, }, secrets: { @@ -82,6 +97,7 @@ describe('connector validation', () => { host: 'localhost', test: 'test', hasAuth: false, + service: 'other', }, } as EmailActionConnector; @@ -91,6 +107,7 @@ describe('connector validation', () => { from: [], port: [], host: [], + service: [], }, }, secrets: { @@ -113,6 +130,7 @@ describe('connector validation', () => { config: { from: 'test@test.com', hasAuth: true, + service: 'other', }, } as EmailActionConnector; @@ -122,6 +140,7 @@ describe('connector validation', () => { from: [], port: ['Port is required.'], host: ['Host is required.'], + service: [], }, }, secrets: { @@ -148,6 +167,7 @@ describe('connector validation', () => { host: 'localhost', test: 'test', hasAuth: true, + service: 'other', }, } as EmailActionConnector; @@ -157,6 +177,7 @@ describe('connector validation', () => { from: [], port: [], host: [], + service: [], }, }, secrets: { @@ -183,6 +204,7 @@ describe('connector validation', () => { host: 'localhost', test: 'test', hasAuth: true, + service: 'other', }, } as EmailActionConnector; @@ -192,6 +214,7 @@ describe('connector validation', () => { from: [], port: [], host: [], + service: [], }, }, secrets: { @@ -202,6 +225,44 @@ describe('connector validation', () => { }, }); }); + test('connector validation fails when server type is not selected', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'password', + }, + id: 'test', + actionTypeId: '.email', + isPreconfigured: false, + name: 'email', + config: { + from: 'test@test.com', + port: 2323, + host: 'localhost', + test: 'test', + hasAuth: true, + }, + }; + + expect( + await actionTypeModel.validateConnector((actionConnector as unknown) as EmailActionConnector) + ).toEqual({ + config: { + errors: { + from: [], + port: [], + host: [], + service: ['Service is required.'], + }, + }, + secrets: { + errors: { + user: [], + password: [], + }, + }, + }); + }); }); describe('action params validation', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx index 5e23754621430..fe0b18b1b2e61 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -7,6 +7,7 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiSelectOption } from '@elastic/eui'; import { ActionTypeModel, ConnectorValidationResult, @@ -14,6 +15,69 @@ import { } from '../../../../types'; import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types'; +const emailServices: EuiSelectOption[] = [ + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.gmailServerTypeLabel', + { + defaultMessage: 'Gmail', + } + ), + value: 'gmail', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.outlookServerTypeLabel', + { + defaultMessage: 'Outlook', + } + ), + value: 'outlook365', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.amazonSesServerTypeLabel', + { + defaultMessage: 'Amazon SES', + } + ), + value: 'ses', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.elasticCloudServerTypeLabel', + { + defaultMessage: 'Elastic Cloud', + } + ), + value: 'elastic_cloud', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.exchangeServerTypeLabel', + { + defaultMessage: 'MS Exchange Server', + } + ), + value: 'exchange_server', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.otherServerTypeLabel', + { + defaultMessage: 'Other', + } + ), + value: 'other', + }, +]; + +export function getEmailServices(isCloudEnabled: boolean) { + return isCloudEnabled + ? emailServices + : emailServices.filter((service) => service.value !== 'elastic_cloud'); +} + export function getActionType(): ActionTypeModel { const mailformat = /^[^@\s]+@[^@\s]+$/; return { @@ -41,6 +105,7 @@ export function getActionType(): ActionTypeModel(), port: new Array(), host: new Array(), + service: new Array(), }; const secretsErrors = { user: new Array(), @@ -69,6 +134,9 @@ export function getActionType(): ActionTypeModel { test('all connector fields is rendered', () => { const actionConnector = { @@ -29,7 +31,7 @@ describe('EmailActionConnectorFields renders', () => { const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -39,6 +41,7 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( 'test@test.com' ); + expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy(); @@ -59,7 +62,7 @@ describe('EmailActionConnectorFields renders', () => { const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -75,6 +78,136 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeFalsy(); }); + test('service field defaults to empty when not defined', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( + 'test@test.com' + ); + expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('select[data-test-subj="emailServiceSelectInput"]').prop('value')).toEqual( + '' + ); + }); + + test('service field is correctly selected when defined', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'gmail', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('select[data-test-subj="emailServiceSelectInput"]').prop('value')).toEqual( + 'gmail' + ); + }); + + test('host, port and secure fields should be disabled when service field is set to well known service', () => { + jest + .spyOn(hooks, 'useEmailConfig') + .mockImplementation(() => ({ emailServiceConfigurable: false, setEmailService: jest.fn() })); + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'gmail', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(true); + expect(wrapper.find('[data-test-subj="emailPortInput"]').first().prop('disabled')).toBe(true); + expect(wrapper.find('[data-test-subj="emailSecureSwitch"]').first().prop('disabled')).toBe( + true + ); + }); + + test('host, port and secure fields should not be disabled when service field is set to other', () => { + jest + .spyOn(hooks, 'useEmailConfig') + .mockImplementation(() => ({ emailServiceConfigurable: true, setEmailService: jest.fn() })); + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'other', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(false); + expect(wrapper.find('[data-test-subj="emailPortInput"]').first().prop('disabled')).toBe(false); + expect(wrapper.find('[data-test-subj="emailSecureSwitch"]').first().prop('disabled')).toBe( + false + ); + }); + test('should display a message to remember username and password when creating a connector with authentication', () => { const actionConnector = { actionTypeId: '.email', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index e4d73ced1eb59..c37c3fc8355b1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiFieldNumber, EuiFieldPassword, + EuiSelect, EuiSwitch, EuiFormRow, EuiTitle, @@ -24,13 +25,22 @@ import { ActionConnectorFieldsProps } from '../../../../types'; import { EmailActionConnector } from '../types'; import { useKibana } from '../../../../common/lib/kibana'; import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import { getEmailServices } from './email'; +import { useEmailConfig } from './use_email_config'; export const EmailActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { - const { docLinks } = useKibana().services; - const { from, host, port, secure, hasAuth } = action.config; + const { docLinks, http, isCloud } = useKibana().services; + const { from, host, port, secure, hasAuth, service } = action.config; const { user, password } = action.secrets; + + const { emailServiceConfigurable, setEmailService } = useEmailConfig( + http, + service, + editActionConfig + ); + useEffect(() => { if (!action.id) { editActionConfig('hasAuth', true); @@ -42,6 +52,8 @@ export const EmailActionConnectorFields: React.FunctionComponent< from !== undefined && errors.from !== undefined && errors.from.length > 0; const isHostInvalid: boolean = host !== undefined && errors.host !== undefined && errors.host.length > 0; + const isServiceInvalid: boolean = + service !== undefined && errors.service !== undefined && errors.service.length > 0; const isPortInvalid: boolean = port !== undefined && errors.port !== undefined && errors.port.length > 0; @@ -93,6 +105,31 @@ export const EmailActionConnectorFields: React.FunctionComponent< + + + { + setEmailService(e.target.value); + }} + /> + + { editActionConfig('secure', e.target.checked); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts index 5da9145ecec0b..df68d0d1237ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts @@ -28,6 +28,13 @@ export const PORT_REQUIRED = i18n.translate( } ); +export const SERVICE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText', + { + defaultMessage: 'Service is required.', + } +); + export const HOST_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts new file mode 100644 index 0000000000000..7d9cf15852748 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { HttpSetup } from 'kibana/public'; +import { useEmailConfig } from './use_email_config'; + +const http = { + get: jest.fn(), +}; + +const editActionConfig = jest.fn(); + +const renderUseEmailConfigHook = (currentService?: string) => + renderHook(() => + useEmailConfig((http as unknown) as HttpSetup, currentService, editActionConfig) + ); + +describe('useEmailConfig', () => { + beforeEach(() => jest.clearAllMocks()); + + it('should call get email config API when service changes and handle result', async () => { + http.get.mockResolvedValueOnce({ + host: 'smtp.gmail.com', + port: 465, + secure: true, + }); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { + result.current.setEmailService('gmail'); + await waitForNextUpdate(); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); + expect(editActionConfig).toHaveBeenCalledWith('service', 'gmail'); + + expect(editActionConfig).toHaveBeenCalledWith('host', 'smtp.gmail.com'); + expect(editActionConfig).toHaveBeenCalledWith('port', 465); + expect(editActionConfig).toHaveBeenCalledWith('secure', true); + + expect(result.current.emailServiceConfigurable).toEqual(false); + }); + + it('should call get email config API when service changes and handle partial result', async () => { + http.get.mockResolvedValueOnce({ + host: 'smtp.gmail.com', + port: 465, + }); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { + result.current.setEmailService('gmail'); + await waitForNextUpdate(); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); + expect(editActionConfig).toHaveBeenCalledWith('service', 'gmail'); + + expect(editActionConfig).toHaveBeenCalledWith('host', 'smtp.gmail.com'); + expect(editActionConfig).toHaveBeenCalledWith('port', 465); + expect(editActionConfig).toHaveBeenCalledWith('secure', false); + + expect(result.current.emailServiceConfigurable).toEqual(false); + }); + + it('should call get email config API when service changes and handle empty result', async () => { + http.get.mockResolvedValueOnce({}); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { + result.current.setEmailService('foo'); + await waitForNextUpdate(); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo'); + expect(editActionConfig).toHaveBeenCalledWith('service', 'foo'); + + expect(editActionConfig).toHaveBeenCalledWith('host', ''); + expect(editActionConfig).toHaveBeenCalledWith('port', 0); + expect(editActionConfig).toHaveBeenCalledWith('secure', false); + + expect(result.current.emailServiceConfigurable).toEqual(true); + }); + + it('should call get email config API when service changes and handle errors', async () => { + http.get.mockImplementationOnce(() => { + throw new Error('no!'); + }); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { + result.current.setEmailService('foo'); + await waitForNextUpdate(); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo'); + expect(editActionConfig).toHaveBeenCalledWith('service', 'foo'); + + expect(editActionConfig).toHaveBeenCalledWith('host', ''); + expect(editActionConfig).toHaveBeenCalledWith('port', 0); + expect(editActionConfig).toHaveBeenCalledWith('secure', false); + + expect(result.current.emailServiceConfigurable).toEqual(true); + }); + + it('should call get email config API when initial service value is passed and determine if config is editable without overwriting config', async () => { + http.get.mockResolvedValueOnce({ + host: 'smtp.gmail.com', + port: 465, + secure: true, + }); + const { result } = renderUseEmailConfigHook('gmail'); + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); + expect(editActionConfig).not.toHaveBeenCalled(); + expect(result.current.emailServiceConfigurable).toEqual(false); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts new file mode 100644 index 0000000000000..fad71cf5d6385 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts @@ -0,0 +1,66 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; +import { HttpSetup } from 'kibana/public'; +import { isEmpty } from 'lodash'; +import { EmailConfig } from '../types'; +import { getServiceConfig } from './api'; + +export function useEmailConfig( + http: HttpSetup, + currentService: string | undefined, + editActionConfig: (property: string, value: unknown) => void +) { + const [emailServiceConfigurable, setEmailServiceConfigurable] = useState(false); + const [emailService, setEmailService] = useState(undefined); + + const getEmailServiceConfig = useCallback( + async (service: string) => { + let serviceConfig: Partial>; + try { + serviceConfig = await getServiceConfig({ http, service }); + setEmailServiceConfigurable(isEmpty(serviceConfig)); + } catch (err) { + serviceConfig = {}; + setEmailServiceConfigurable(true); + } + + return serviceConfig; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [editActionConfig] + ); + + useEffect(() => { + (async () => { + if (emailService) { + const serviceConfig = await getEmailServiceConfig(emailService); + + editActionConfig('service', emailService); + editActionConfig('host', serviceConfig?.host ? serviceConfig.host : ''); + editActionConfig('port', serviceConfig?.port ? serviceConfig.port : 0); + editActionConfig('secure', null != serviceConfig?.secure ? serviceConfig.secure : false); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [emailService]); + + useEffect(() => { + (async () => { + if (currentService) { + await getEmailServiceConfig(currentService); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentService]); + + return { + emailServiceConfigurable, + setEmailService, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 50410ba3c153d..60e0a0f14b897 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -78,6 +78,7 @@ export interface EmailConfig { port: number; secure?: boolean; hasAuth: boolean; + service: string; } export interface EmailSecrets { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index cc04b8e7871cd..bed7b09110d87 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -11,7 +11,7 @@ export { BASE_ALERTING_API_PATH, INTERNAL_BASE_ALERTING_API_PATH, } from '../../../../alerting/common'; -export { BASE_ACTION_API_PATH } from '../../../../actions/common'; +export { BASE_ACTION_API_PATH, INTERNAL_BASE_ACTION_API_PATH } from '../../../../actions/common'; export type Section = 'connectors' | 'rules'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts index 2985a5306ed51..de64906f75de3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts @@ -40,6 +40,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { list: jest.fn(), } as ActionTypeRegistryContract, charts: chartPluginMock.createStartContract(), + isCloud: false, kibanaFeatures: [], element: ({ style: { cursor: 'pointer' }, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 7661eefba7f65..17f0766a826e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -66,6 +66,7 @@ export interface TriggersAndActionsUIPublicPluginStart { interface PluginsSetup { management: ManagementSetup; home?: HomePublicPluginSetup; + cloud?: { isCloudEnabled: boolean }; } interface PluginsStart { @@ -148,6 +149,7 @@ export class Plugin charts: pluginsStart.charts, alerting: pluginsStart.alerting, spaces: pluginsStart.spaces, + isCloud: Boolean(plugins.cloud?.isCloudEnabled), element: params.element, storage: new Storage(window.localStorage), setBreadcrumbs: params.setBreadcrumbs, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx index 34f56a65df3e8..0e2f10b96fe6d 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx @@ -168,7 +168,7 @@ export const BrowserSimpleFields = memo(({ validate }) => { helpText={ } > diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx index 8eb81eb92f7b4..c4de1d53fe998 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx @@ -186,7 +186,7 @@ export const HTTPSimpleFields = memo(({ validate }) => { helpText={ } > diff --git a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx index 420f218429e40..92afe4c5072e1 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx @@ -190,7 +190,7 @@ export const ICMPSimpleFields = memo(({ validate }) => { helpText={ } > diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx index 8bc017a51cfa9..37f0c82595e02 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx @@ -157,7 +157,7 @@ export const TCPSimpleFields = memo(({ validate }) => { helpText={ } > diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 5a9d2a20fee16..dd43606cc79b7 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -149,7 +149,11 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), ...(options.publicBaseUrl ? ['--server.publicBaseUrl=https://localhost:5601'] : []), - `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, + `--xpack.actions.allowedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + 'smtp.live.com', + ])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', '--xpack.alerting.invalidateApiKeysTask.interval="15s"', '--xpack.alerting.healthCheck.interval="1s"', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts index 917246f09a99e..b3829824b7971 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts @@ -319,5 +319,122 @@ export default function emailTest({ getService }: FtrProviderContext) { }); }); }); + + it('should return 200 when creating an email action without defining service', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An email action', + connector_type_id: '.email', + config: { + from: 'bob@example.com', + host: 'some.non.existent.com', + port: 25, + hasAuth: true, + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + name: 'An email action', + connector_type_id: '.email', + is_missing_secrets: false, + config: { + service: 'other', + hasAuth: true, + host: 'some.non.existent.com', + port: 25, + secure: null, + from: 'bob@example.com', + }, + }); + + expect(typeof createdAction.id).to.be('string'); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + name: 'An email action', + connector_type_id: '.email', + is_missing_secrets: false, + config: { + from: 'bob@example.com', + service: 'other', + hasAuth: true, + host: 'some.non.existent.com', + port: 25, + secure: null, + }, + }); + }); + + it('should return 200 when creating an email action with nodemailer well-defined service', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An email action', + connector_type_id: '.email', + config: { + from: 'bob@example.com', + service: 'hotmail', + hasAuth: true, + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + name: 'An email action', + connector_type_id: '.email', + is_missing_secrets: false, + config: { + service: 'hotmail', + hasAuth: true, + host: null, + port: null, + secure: null, + from: 'bob@example.com', + }, + }); + + expect(typeof createdAction.id).to.be('string'); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + name: 'An email action', + connector_type_id: '.email', + is_missing_secrets: false, + config: { + from: 'bob@example.com', + service: 'hotmail', + hasAuth: true, + host: null, + port: null, + secure: null, + }, + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts index 811a9470611eb..9b88dace13239 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts @@ -64,5 +64,23 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(responseWithisMissingSecrets.status).to.eql(200); expect(responseWithisMissingSecrets.body.isMissingSecrets).to.eql(false); }); + + it('7.16.0 migrates email connector configurations to set `service` property if not set', async () => { + const connectorWithService = await supertest.get( + `${getUrlPrefix(``)}/api/actions/action/0f8f2810-0a59-11ec-9a7c-fd0c2b83ff7c` + ); + + expect(connectorWithService.status).to.eql(200); + expect(connectorWithService.body.config).key('service'); + expect(connectorWithService.body.config.service).to.eql('someservice'); + + const connectorWithoutService = await supertest.get( + `${getUrlPrefix(``)}/api/actions/action/1e0824a0-0a59-11ec-9a7c-fd0c2b83ff7c` + ); + + expect(connectorWithoutService.status).to.eql(200); + expect(connectorWithoutService.body.config).key('service'); + expect(connectorWithoutService.body.config.service).to.eql('other'); + }); }); } diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts index 06d966851abfd..293b0e94351d0 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -184,7 +184,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body.length).to.eql( expectedResponse.length, - `Response body should have ${expectedResponse.length} entries (got ${body})` + `Response body should have ${expectedResponse.length} entries (got ${JSON.stringify(body)})` ); for (const entry of expectedResponse) { const responseEntry = body.find((obj: any) => obj.id === entry.id); diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index ef822b0af2a29..eee1f0be5ba37 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -70,6 +70,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', + `--xpack.securitySolution.alertIgnoreFields=${JSON.stringify([ + 'testing_ignored.constant', + '/testing_regex*/', + ])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields" ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), ...(ssl ? [ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts new file mode 100644 index 0000000000000..409128523ea40 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts @@ -0,0 +1,133 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getEqlRuleForSignalTesting, + getSignalsById, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, +} from '../../utils'; + +interface Ignore { + normal_constant?: string; + small_field?: string; + testing_ignored?: string; + testing_regex?: string; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + /** + * See the config file (detection_engine_api_integration/common/config.ts) for which field values were added to be ignored + * for testing. The values should be in the config around the area of: + * --xpack.securitySolution.alertIgnoreFields=[testing.ignore_1,/[testingRegex] + * meaning that the ignore fields values should be the array: ["testing.ignore_1", "/[testingRegex]/"] + * + * This test exercises the ability to be able to ignore particular values within the fields API and merge strategies. + * These values can be defined in your kibana.yml file as "xpack.securitySolution.alertIgnoreFields". This is useful + * for users that find bugs or regressions within query languages or bugs within the merge strategies + * where one or more fields are causing problems and they need to turn disable that particular field. + * + * Ref: + * https://github.com/elastic/kibana/issues/110802 + * https://github.com/elastic/elasticsearch/issues/77152 + * + * Files ref: + * server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts + * server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts + */ + describe('ignore_fields', () => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ignore_fields'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/ignore_fields'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + }); + + it('should ignore the field of "testing_ignored"', async () => { + const rule = getEqlRuleForSignalTesting(['ignore_fields']); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits + .map((hit) => (hit._source as Ignore).testing_ignored) + .sort(); + + // Value should be "undefined for all records" + expect(hits).to.eql([undefined, undefined, undefined, undefined]); + }); + + it('should ignore the field of "testing_regex"', async () => { + const rule = getEqlRuleForSignalTesting(['ignore_fields']); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => (hit._source as Ignore).testing_regex).sort(); + + // Value should be "undefined for all records" + expect(hits).to.eql([undefined, undefined, undefined, undefined]); + }); + + it('should have the field of "normal_constant"', async () => { + const rule = getEqlRuleForSignalTesting(['ignore_fields']); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits + .map((hit) => (hit._source as Ignore).normal_constant) + .sort(); + + // Value should be "constant_value for all records" + expect(hits).to.eql(['constant_value', 'constant_value', 'constant_value', 'constant_value']); + }); + + // TODO: Remove this test once https://github.com/elastic/elasticsearch/issues/77152 is fixed + it('should ignore the field of "_ignored" when using EQL and index the data', async () => { + const rule = getEqlRuleForSignalTesting(['ignore_fields']); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => (hit._source as Ignore).small_field).sort(); + + // We just test a constant value to ensure this did not blow up on us and did index data. + expect(hits).to.eql([ + '1 indexed', + '2 large not indexed', + '3 large not indexed', + '4 large not indexed', + ]); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 27474fe563a36..41a3d084e084e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -48,6 +48,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./timestamps')); loadTestFile(require.resolve('./runtime')); loadTestFile(require.resolve('./throttle')); + loadTestFile(require.resolve('./ignore_fields')); }); // That split here enable us on using a different ciGroup to run the tests diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/table.ts index da079c0976db3..892534eec7033 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/table.ts @@ -122,7 +122,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); }); + it('should keep the coloring consistent when changing mode', async () => { + // Change mode from percent to number + await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_number'); + await PageObjects.header.waitUntilLoadingHasFinished(); + // check that all remained the same + const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); + expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); + }); + + it('should keep the coloring consistent when moving to custom palette from default', async () => { + await PageObjects.lens.changePaletteTo('custom'); + await PageObjects.header.waitUntilLoadingHasFinished(); + // check that all remained the same + const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); + expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); + }); + it('tweak the color stops numeric value', async () => { + // restore default palette and percent mode + await PageObjects.lens.changePaletteTo('temperature'); + await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_percent'); + // now tweak the value await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '30', { clearWithKeyboard: true, }); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index d351e8f7057e4..5f8d346ee4473 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -114,7 +114,8 @@ export default function ({ getService }: FtrProviderContext) { }, ]; - describe('job on data set with date_nanos time field', function () { + // test skipped until https://github.com/elastic/elasticsearch/pull/77109 is fixed + describe.skip('job on data set with date_nanos time field', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/event_rate_nanos'); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts new file mode 100644 index 0000000000000..d7a563e8c355f --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts @@ -0,0 +1,314 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; +import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import type { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; + +const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_1_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + }, + ], + influencers: [], + }, + analysis_limits: { + model_memory_limit: '10mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_1_smv', + job_id: 'fq_single_1_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_2_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'low_mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'low_mean(responsetime)', + function: 'low_mean', + field_name: 'responsetime', + }, + ], + influencers: ['responsetime'], + }, + analysis_limits: { + model_memory_limit: '11mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_2_smv', + job_id: 'fq_single_2_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_3_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'high_mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'high_mean(responsetime)', + function: 'high_mean', + field_name: 'responsetime', + }, + ], + influencers: ['responsetime'], + }, + analysis_limits: { + model_memory_limit: '11mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_3_smv', + job_id: 'fq_single_3_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, +]; + +const testDFAJobs: DataFrameAnalyticsConfig[] = [ + // @ts-expect-error not full interface + { + id: `bm_1_1`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + source: { + index: ['ft_bank_marketing'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-bm_1_1', + results_field: 'ml', + }, + analysis: { + classification: { + prediction_field_name: 'test', + dependent_variable: 'y', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + // @ts-expect-error not full interface + { + id: `ihp_1_2`, + description: 'This is the job description', + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-ihp_1_2', + results_field: 'ml', + }, + analysis: { + outlier_detection: {}, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '5mb', + }, + // @ts-expect-error not full interface + { + id: `egs_1_3`, + description: 'This is the job description', + source: { + index: ['ft_egs_regression'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-egs_1_3', + results_field: 'ml', + }, + analysis: { + regression: { + prediction_field_name: 'test', + dependent_variable: 'stab', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '20mb', + }, +]; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('export jobs', function () { + this.tags(['mlqa']); + before(async () => { + await ml.api.cleanMlIndices(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/egs_regression'); + await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); + + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const { job, datafeed } of testADJobs) { + await ml.api.createAnomalyDetectionJob(job); + await ml.api.createDatafeed(datafeed); + } + for (const job of testDFAJobs) { + await ml.api.createDataFrameAnalyticsJob(job); + } + + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToStackManagement(); + await ml.navigation.navigateToStackManagementJobsListPage(); + }); + after(async () => { + await ml.api.cleanMlIndices(); + ml.stackManagementJobs.deleteExportedFiles([ + 'anomaly_detection_jobs', + 'data_frame_analytics_jobs', + ]); + }); + + it('opens export flyout and exports anomaly detector jobs', async () => { + await ml.stackManagementJobs.openExportFlyout(); + await ml.stackManagementJobs.selectExportJobType('anomaly-detector'); + await ml.stackManagementJobs.selectExportJobSelectAll('anomaly-detector'); + await ml.stackManagementJobs.selectExportJobs(); + await ml.stackManagementJobs.assertExportedADJobsAreCorrect(testADJobs); + }); + + it('opens export flyout and exports data frame analytics jobs', async () => { + await ml.stackManagementJobs.openExportFlyout(); + await ml.stackManagementJobs.selectExportJobType('data-frame-analytics'); + await ml.stackManagementJobs.selectExportJobSelectAll('data-frame-analytics'); + await ml.stackManagementJobs.selectExportJobs(); + await ml.stackManagementJobs.assertExportedDFAJobsAreCorrect(testDFAJobs); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json new file mode 100644 index 0000000000000..1bc51d433858e --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json @@ -0,0 +1,213 @@ +[ + { + "job": { + "job_id": "ad-test1", + "description": "", + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name": "doc_count", + "detectors": [ + { + "detector_description": "mean(responsetime)", + "function": "mean", + "field_name": "responsetime", + "detector_index": 0 + } + ], + "influencers": [] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": true, + "annotations_enabled": true + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-ad-test1", + "job_id": "ad-test1", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "ft_farequote" + ], + "aggregations": { + "buckets": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "90000ms" + }, + "aggregations": { + "responsetime": { + "avg": { + "field": "responsetime" + } + }, + "@timestamp": { + "max": { + "field": "@timestamp" + } + } + } + } + }, + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + }, + { + "job": { + "job_id": "ad-test2", + "groups": [ + "newgroup" + ], + "description": "", + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name": "doc_count", + "detectors": [ + { + "detector_description": "mean(responsetime)", + "function": "mean", + "field_name": "responsetime", + "detector_index": 0 + } + ], + "influencers": [] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": true, + "annotations_enabled": true + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-ad-test2", + "job_id": "ad-test2", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "missing" + ], + "aggregations": { + "buckets": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "90000ms" + }, + "aggregations": { + "responsetime": { + "avg": { + "field": "responsetime" + } + }, + "@timestamp": { + "max": { + "field": "@timestamp" + } + } + } + } + }, + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + }, + { + "job": { + "job_id": "ad-test3", + "custom_settings": {}, + "description": "", + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "mean(responsetime) partitionfield=airline", + "function": "mean", + "field_name": "responsetime", + "partition_field_name": "airline", + "detector_index": 0 + } + ], + "influencers": [ + "airline" + ] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": false, + "annotations_enabled": false + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-ad-test3", + "job_id": "ad-test3", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "ft_farequote" + ], + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + } +] diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json new file mode 100644 index 0000000000000..5c40480832c00 --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json @@ -0,0 +1 @@ +Hey! this isn't JSON. diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json new file mode 100644 index 0000000000000..cb93aa9e24c5f --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json @@ -0,0 +1,60 @@ +[ + { + "id": "dfa-test1", + "description": "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + "source": { + "index": [ + "ft_bank_marketing" + ], + "query": { + "match_all": {} + } + }, + "dest": { + "index": "user-dfa-test1", + "results_field": "ml" + }, + "analysis": { + "classification": { + "prediction_field_name": "user-test", + "dependent_variable": "y", + "training_percent": 20 + } + }, + "analyzed_fields": { + "includes": [], + "excludes": [] + }, + "model_memory_limit": "60mb", + "allow_lazy_start": false + }, + { + "id": "dfa-test2", + "description": "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + "source": { + "index": [ + "missing-index" + ], + "query": { + "match_all": {} + } + }, + "dest": { + "index": "user-dfa-test2", + "results_field": "ml" + }, + "analysis": { + "classification": { + "prediction_field_name": "test", + "dependent_variable": "y", + "training_percent": 20 + } + }, + "analyzed_fields": { + "includes": [], + "excludes": [] + }, + "model_memory_limit": "60mb", + "allow_lazy_start": false + } +] diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts new file mode 100644 index 0000000000000..6211885af0a2a --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -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 path from 'path'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { JobType } from '../../../../../plugins/ml/common/types/saved_objects'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const testDataListPositive = [ + { + filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs_7.16.json'), + expected: { + jobType: 'anomaly-detector' as JobType, + jobIds: ['ad-test1', 'ad-test3'], + skippedJobIds: ['ad-test2'], + }, + }, + { + filePath: path.join(__dirname, 'files_to_import', 'data_frame_analytics_jobs_7.16.json'), + expected: { + jobType: 'data-frame-analytics' as JobType, + jobIds: ['dfa-test1'], + skippedJobIds: ['dfa-test2'], + }, + }, + ]; + + describe('import jobs', function () { + this.tags(['mlqa']); + before(async () => { + await ml.api.cleanMlIndices(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToStackManagement(); + await ml.navigation.navigateToStackManagementJobsListPage(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataListPositive) { + it('selects and reads file', async () => { + await ml.testExecution.logTestStep('selects job import'); + await ml.stackManagementJobs.openImportFlyout(); + await ml.stackManagementJobs.selectFileToImport(testData.filePath); + }); + it('has the correct importable jobs', async () => { + await ml.stackManagementJobs.assertCorrectTitle( + [...testData.expected.jobIds, ...testData.expected.skippedJobIds].length, + testData.expected.jobType + ); + await ml.stackManagementJobs.assertJobIdsExist(testData.expected.jobIds); + await ml.stackManagementJobs.assertJobIdsSkipped(testData.expected.skippedJobIds); + }); + + it('imports jobs', async () => { + await ml.stackManagementJobs.importJobs(); + }); + + it('ensures jobs have been imported', async () => { + if (testData.expected.jobType === 'anomaly-detector') { + await ml.navigation.navigateToStackManagementJobsListPageAnomalyDetectionTab(); + await ml.jobTable.refreshJobList(); + for (const id of testData.expected.jobIds) { + await ml.jobTable.filterWithSearchString(id); + } + for (const id of testData.expected.skippedJobIds) { + await ml.jobTable.filterWithSearchString(id, 0); + } + } else { + await ml.navigation.navigateToStackManagementJobsListPageAnalyticsTab(); + await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); + for (const id of testData.expected.jobIds) { + await ml.dataFrameAnalyticsTable.assertAnalyticsJobDisplayedInTable(id, true); + } + for (const id of testData.expected.skippedJobIds) { + await ml.dataFrameAnalyticsTable.assertAnalyticsJobDisplayedInTable(id, false); + } + } + }); + } + + describe('correctly fails to import bad data', async () => { + it('selects and reads file', async () => { + await ml.testExecution.logTestStep('selects job import'); + await ml.stackManagementJobs.openImportFlyout(); + await ml.stackManagementJobs.selectFileToImport( + path.join(__dirname, 'files_to_import', 'bad_data.json'), + true + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts index f120ab0b450dc..c5e0728266bab 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts @@ -13,5 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./synchronize')); loadTestFile(require.resolve('./manage_spaces')); + loadTestFile(require.resolve('./import_jobs')); + loadTestFile(require.resolve('./export_jobs')); }); } diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts index c43747c346ca7..1e629927ffb4d 100644 --- a/x-pack/test/functional/apps/visualize/reporting.ts +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -42,13 +42,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('Print PDF button', () => { - it('is not available if new', async () => { + it('is available if new', async () => { await PageObjects.common.navigateToUrl('visualize', 'new', { useActualUrl: true }); await PageObjects.visualize.clickAggBasedVisualizations(); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch('ecommerce'); await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); it('becomes available when saved', async () => { diff --git a/x-pack/test/functional/es_archives/actions/data.json b/x-pack/test/functional/es_archives/actions/data.json index 18d67da1752bc..31d10005c0939 100644 --- a/x-pack/test/functional/es_archives/actions/data.json +++ b/x-pack/test/functional/es_archives/actions/data.json @@ -110,3 +110,67 @@ } } } + +{ + "type": "doc", + "value": { + "id": "action:0f8f2810-0a59-11ec-9a7c-fd0c2b83ff7c", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId" : ".email", + "name" : "test email connector with auth", + "isMissingSecrets" : false, + "config" : { + "hasAuth" : true, + "from" : "me@me.com", + "host" : "smtp.myhost.com", + "port" : 25, + "service" : "someservice", + "secure" : null + }, + "secrets" : "V2EJEtTv3yTFi1kdglhNahnKYWCS+J7aWCJQU+eEqGPZEz6n7G1NsBWoh7IY0FteLTilTteQXyY/Eg3k/7bb0G8Mz+WBZ1mRvUggGTFqgoOptyUsvHoBhv0R/1bCTCabN3Pe88AfnC+VDXqwuMifpmgKEEsKF3H8VONv7TYO02FW" + }, + "migrationVersion": { + "action": "7.14.0" + }, + "coreMigrationVersion" : "7.15.0", + "references": [ + ], + "type": "action", + "updated_at": "2021-08-31T12:43:37.117Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "action:1e0824a0-0a59-11ec-9a7c-fd0c2b83ff7c", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId" : ".email", + "name" : "test email connector no auth", + "isMissingSecrets" : false, + "config" : { + "hasAuth" : false, + "from" : "you@you.com", + "host" : "smtp.you.com", + "port" : 485, + "secure" : true, + "service" : null + }, + "secrets" : "iw/bRTXZQXOV0ODocb6FQnHR6AyeVyD91We03llNStyTNFwuHVWdFl6ZdiEEeDOadBMeJomvp/dAfQevGpbwWdclcu9F87x3CfeGqV9DtBy0dXRbx9PzKBwgJdK3ucHQDFAs8ZXQbefvCOFjCHGAsJDPhTKj5rTUyg==" + }, + "migrationVersion": { + "action": "7.14.0" + }, + "coreMigrationVersion" : "7.15.0", + "references": [ + ], + "type": "action", + "updated_at": "2021-08-31T12:44:01.396Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/actions/mappings.json b/x-pack/test/functional/es_archives/actions/mappings.json index 737e0df57552e..8289174ffd57d 100644 --- a/x-pack/test/functional/es_archives/actions/mappings.json +++ b/x-pack/test/functional/es_archives/actions/mappings.json @@ -572,6 +572,9 @@ } } }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { "properties": { "description": { diff --git a/x-pack/test/functional/es_archives/event_log_legacy_ids/data.json b/x-pack/test/functional/es_archives/event_log_legacy_ids/data.json new file mode 100644 index 0000000000000..54b0c3f48189a --- /dev/null +++ b/x-pack/test/functional/es_archives/event_log_legacy_ids/data.json @@ -0,0 +1,164 @@ +{ + "type": "doc", + "value": { + "id": "X6bLb3UBt6Z_MVvSTfYk", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.933Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.933Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.933Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "621f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "version": "8.0.0" + }, + "message": "test 2020-10-28T15:19:55.913Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "X6bLb3UBt6Z_MVvSTfYk0000", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.933Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test legacy", + "duration": 0, + "end": "2020-10-28T15:19:55.933Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.933Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "521f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "version": "7.14.0" + }, + "message": "test legacy 2020-10-28T15:19:55.913Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "YKbLb3UBt6Z_MVvSTfY8", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.957Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.957Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.957Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "621f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "version": "8.0.0" + }, + "message": "test 2020-10-28T15:19:55.938Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "YabLb3UBt6Z_MVvSTfZc0000", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.991Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.991Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.991Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "521f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "version": "7.0.0" + }, + "message": "test legacy 2020-10-28T15:19:55.962Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "YabLb3UBt6Z_MVvSTfZc", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.991Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.991Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.991Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "621f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "version": "8.0.0" + }, + "message": "test 2020-10-28T15:19:55.962Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/event_log_legacy_ids/mappings.json b/x-pack/test/functional/es_archives/event_log_legacy_ids/mappings.json new file mode 100644 index 0000000000000..564a9e56a7853 --- /dev/null +++ b/x-pack/test/functional/es_archives/event_log_legacy_ids/mappings.json @@ -0,0 +1,579 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "eaf6f5841dbf4cb5e3045860f75f53ca", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", + "event_log_test": "bef808d4a9c27f204ffbda3359233931", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "f74dfe498e1849267cda41580b2be110", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "event_log_test": { + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "monitoring-telemetry": { + "properties": { + "reportedClusterUuids": { + "type": "keyword" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana-event-log-7.9.0": { + "is_write_index": true + } + }, + "index": ".kibana-event-log-7.9.0-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "message": { + "norms": false, + "type": "text" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + } + } + }, + "kibana": { + "properties": { + "alerting": { + "properties": { + "instance_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "saved_objects": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "rel": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "server_uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "tags": { + "ignore_above": 1024, + "meta": { + "isArray": "true" + }, + "type": "keyword" + }, + "user": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "lifecycle": { + "name": "kibana-event-log-policy", + "rollover_alias": ".kibana-event-log-7.9.0" + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana-event-log-8.0.0": { + "is_write_index": true + } + }, + "index": ".kibana-event-log-8.0.0-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "message": { + "norms": false, + "type": "text" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + } + } + }, + "kibana": { + "properties": { + "alerting": { + "properties": { + "instance_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "saved_objects": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "rel": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "server_uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "type": "version" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "tags": { + "ignore_above": 1024, + "meta": { + "isArray": "true" + }, + "type": "keyword" + }, + "user": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "lifecycle": { + "name": "kibana-event-log-policy", + "rollover_alias": ".kibana-event-log-8.0.0" + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json b/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json index 4e871f6308b77..70bb1d8551a10 100644 --- a/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json +++ b/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json @@ -202,7 +202,8 @@ "type": "event_log_test" } ], - "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "version": "8.0.0" }, "message": "test 2020-10-28T15:19:55.913Z" } @@ -234,7 +235,8 @@ "type": "event_log_test" } ], - "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "version": "8.0.0" }, "message": "test 2020-10-28T15:19:55.938Z" } @@ -266,7 +268,8 @@ "type": "event_log_test" } ], - "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "version": "8.0.0" }, "message": "test 2020-10-28T15:19:55.962Z" } diff --git a/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json b/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json index e9a709138256a..15382fb2524fe 100644 --- a/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json +++ b/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json @@ -397,7 +397,7 @@ "server_uuid": { "ignore_above": 1024, "type": "keyword" - } + } } }, "message": { @@ -531,6 +531,9 @@ "server_uuid": { "ignore_above": 1024, "type": "keyword" + }, + "version": { + "type": "version" } } }, diff --git a/x-pack/test/functional/es_archives/security_solution/ignore_fields/data.json b/x-pack/test/functional/es_archives/security_solution/ignore_fields/data.json new file mode 100644 index 0000000000000..7a33785c0bc41 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/ignore_fields/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "ignore_fields", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "small_field": "1 indexed" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "ignore_fields", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "small_field": "2 large not indexed" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "ignore_fields", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "small_field": "3 large not indexed" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "ignore_fields", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "small_field": "4 large not indexed" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/security_solution/ignore_fields/mappings.json b/x-pack/test/functional/es_archives/security_solution/ignore_fields/mappings.json new file mode 100644 index 0000000000000..e2c8ca3c2bc89 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/ignore_fields/mappings.json @@ -0,0 +1,41 @@ +{ + "type": "index", + "value": { + "index": "ignore_fields", + "mappings": { + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "testing_ignored": { + "properties": { + "constant": { + "type": "constant_keyword", + "value": "constant_value" + } + } + }, + "testing_regex": { + "type": "constant_keyword", + "value": "constant_value" + }, + "normal_constant": { + "type": "constant_keyword", + "value": "constant_value" + }, + "small_field": { + "type": "keyword", + "ignore_above": 10 + } + } + }, + "settings": { + "index": { + "refresh_interval": "1s", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts index 48fb89e51ff11..45b9fa2f29ccd 100644 --- a/x-pack/test/functional/services/ml/stack_management_jobs.ts +++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts @@ -6,10 +6,16 @@ */ import expect from '@kbn/expect'; +import { REPO_ROOT } from '@kbn/utils'; +import fs from 'fs'; +import path from 'path'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { MlADJobTable } from './job_table'; -import { MlDFAJobTable } from './data_frame_analytics_table'; +import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { MlADJobTable } from './job_table'; +import type { MlDFAJobTable } from './data_frame_analytics_table'; +import type { JobType } from '../../../../plugins/ml/common/types/saved_objects'; +import type { Job, Datafeed } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import type { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; type SyncFlyoutObjectType = | 'MissingObjects' @@ -18,7 +24,7 @@ type SyncFlyoutObjectType = | 'ObjectsUnmatchedDatafeed'; export function MachineLearningStackManagementJobsProvider( - { getService }: FtrProviderContext, + { getService, getPageObjects }: FtrProviderContext, mlADJobTable: MlADJobTable, mlDFAJobTable: MlDFAJobTable ) { @@ -26,6 +32,9 @@ export function MachineLearningStackManagementJobsProvider( const retry = getService('retry'); const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); + const log = getService('log'); + + const PageObjects = getPageObjects(['common']); return { async openSyncFlyout() { @@ -194,5 +203,212 @@ export function MachineLearningStackManagementJobsProvider( } await this.assertSpaceSelectionRowSelected(spaceId, shouldSelect); }, + + async openImportFlyout() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('mlJobsImportButton', 1000); + await testSubjects.existOrFail('mlJobMgmtImportJobsFlyout'); + }); + }, + + async openExportFlyout() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('mlJobsExportButton', 1000); + await testSubjects.existOrFail('mlJobMgmtExportJobsFlyout'); + }); + }, + + async selectFileToImport(filePath: string, expectError: boolean = false) { + log.debug(`Importing file '${filePath}' ...`); + await PageObjects.common.setFileInputPath(filePath); + + if (expectError) { + await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + } else { + await testSubjects.missingOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + await testSubjects.existOrFail('mlJobMgmtImportJobsFileRead'); + } + }, + + async assertJobIdsExist(expectedJobIds: string[]) { + const inputs = await testSubjects.findAll('mlJobMgmtImportJobIdInput'); + const actualJobIds = await Promise.all(inputs.map((i) => i.getAttribute('value'))); + + expect(actualJobIds.sort()).to.eql( + expectedJobIds.sort(), + `Expected job ids to be '${JSON.stringify(expectedJobIds)}' (got '${JSON.stringify( + actualJobIds + )}')` + ); + }, + + async assertCorrectTitle(jobCount: number, jobType: JobType) { + const dataTestSubj = + jobType === 'anomaly-detector' + ? 'mlJobMgmtImportJobsADTitle' + : 'mlJobMgmtImportJobsDFATitle'; + const subj = await testSubjects.find(dataTestSubj); + const title = (await subj.parseDomContent()).html(); + + const jobTypeString = + jobType === 'anomaly-detector' ? 'anomaly detection' : 'data frame analytics'; + + const results = title.match( + /(\d) (anomaly detection|data frame analytics) job[s]? read from file$/ + ); + expect(results).to.not.eql(null, `Expected regex results to not be null`); + const foundCount = results![1]; + const foundJobTypeString = results![2]; + expect(foundCount).to.eql( + jobCount, + `Expected job count to be '${jobCount}' (got '${foundCount}')` + ); + expect(foundJobTypeString).to.eql( + jobTypeString, + `Expected job count to be '${jobTypeString}' (got '${foundJobTypeString}')` + ); + }, + + async assertJobIdsSkipped(expectedJobIds: string[]) { + const subj = await testSubjects.find('mlJobMgmtImportJobsCannotBeImportedCallout'); + const skippedJobTitles = await subj.findAllByTagName('h5'); + const actualJobIds = ( + await Promise.all(skippedJobTitles.map((i) => i.parseDomContent())) + ).map((t) => t.html()); + + expect(actualJobIds.sort()).to.eql( + expectedJobIds.sort(), + `Expected job ids to be '${JSON.stringify(expectedJobIds)}' (got '${JSON.stringify( + actualJobIds + )}')` + ); + }, + + async importJobs() { + await testSubjects.click('mlJobMgmtImportImportButton', 1000); + await testSubjects.missingOrFail('mlJobMgmtImportJobsFlyout', { timeout: 60 * 1000 }); + }, + + async assertReadErrorCalloutExists() { + await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + }, + + async selectExportJobType(jobType: JobType) { + if (jobType === 'anomaly-detector') { + await testSubjects.click('mlJobMgmtExportJobsADTab'); + await testSubjects.existOrFail('mlJobMgmtExportJobsADJobList'); + } else { + await testSubjects.click('mlJobMgmtExportJobsDFATab'); + await testSubjects.existOrFail('mlJobMgmtExportJobsDFAJobList'); + } + }, + + async selectExportJobSelectAll(jobType: JobType) { + await testSubjects.click('mlJobMgmtExportJobsSelectAllButton'); + const subjLabel = + jobType === 'anomaly-detector' + ? 'mlJobMgmtExportJobsADJobList' + : 'mlJobMgmtExportJobsDFAJobList'; + const subj = await testSubjects.find(subjLabel); + const inputs = await subj.findAllByTagName('input'); + const allInputValues = await Promise.all(inputs.map((input) => input.getAttribute('value'))); + expect(allInputValues.every((i) => i === 'on')).to.eql( + true, + `Expected all inputs to be checked` + ); + }, + + async getDownload(filePath: string) { + return retry.tryForTime(5000, async () => { + expect(fs.existsSync(filePath)).to.be(true); + return fs.readFileSync(filePath).toString(); + }); + }, + + getExportedFile(fileName: string) { + return path.resolve(REPO_ROOT, `target/functional-tests/downloads/${fileName}.json`); + }, + + deleteExportedFiles(fileNames: string[]) { + fileNames.forEach((file) => { + try { + fs.unlinkSync(this.getExportedFile(file)); + } catch (e) { + // it might not have been there to begin with + } + }); + }, + + async selectExportJobs() { + await testSubjects.click('mlJobMgmtExportExportButton'); + await testSubjects.missingOrFail('mlJobMgmtExportJobsFlyout', { timeout: 60 * 1000 }); + }, + + async assertExportedADJobsAreCorrect(expectedJobs: Array<{ job: Job; datafeed: Datafeed }>) { + const file = JSON.parse( + await this.getDownload(this.getExportedFile('anomaly_detection_jobs')) + ); + const loadedFile = Array.isArray(file) ? file : [file]; + const sortedActualJobs = loadedFile.sort((a, b) => a.job.job_id.localeCompare(b.job.job_id)); + + const sortedExpectedJobs = expectedJobs.sort((a, b) => + a.job.job_id.localeCompare(b.job.job_id) + ); + expect(sortedActualJobs.length).to.eql( + sortedExpectedJobs.length, + `Expected length of exported jobs to be '${sortedExpectedJobs.length}' (got '${sortedActualJobs.length}')` + ); + + sortedExpectedJobs.forEach((expectedJob, i) => { + expect(sortedActualJobs[i].job.job_id).to.eql( + expectedJob.job.job_id, + `Expected job id to be '${expectedJob.job.job_id}' (got '${sortedActualJobs[i].job.job_id}')` + ); + expect(sortedActualJobs[i].job.analysis_config.detectors.length).to.eql( + expectedJob.job.analysis_config.detectors.length, + `Expected detectors length to be '${expectedJob.job.analysis_config.detectors.length}' (got '${sortedActualJobs[i].job.analysis_config.detectors.length}')` + ); + expect(sortedActualJobs[i].job.analysis_config.detectors[0].function).to.eql( + expectedJob.job.analysis_config.detectors[0].function, + `Expected first detector function to be '${expectedJob.job.analysis_config.detectors[0].function}' (got '${sortedActualJobs[i].job.analysis_config.detectors[0].function}')` + ); + expect(sortedActualJobs[i].datafeed.datafeed_id).to.eql( + expectedJob.datafeed.datafeed_id, + `Expected job id to be '${expectedJob.datafeed.datafeed_id}' (got '${sortedActualJobs[i].datafeed.datafeed_id}')` + ); + }); + }, + + async assertExportedDFAJobsAreCorrect(expectedJobs: DataFrameAnalyticsConfig[]) { + const file = JSON.parse( + await this.getDownload(this.getExportedFile('data_frame_analytics_jobs')) + ); + const loadedFile = Array.isArray(file) ? file : [file]; + const sortedActualJobs = loadedFile.sort((a, b) => a.id.localeCompare(b.id)); + + const sortedExpectedJobs = expectedJobs.sort((a, b) => a.id.localeCompare(b.id)); + + expect(sortedActualJobs.length).to.eql( + sortedExpectedJobs.length, + `Expected length of exported jobs to be '${sortedExpectedJobs.length}' (got '${sortedActualJobs.length}')` + ); + + sortedExpectedJobs.forEach((expectedJob, i) => { + expect(sortedActualJobs[i].id).to.eql( + expectedJob.id, + `Expected job id to be '${expectedJob.id}' (got '${sortedActualJobs[i].id}')` + ); + const expectedType = Object.keys(expectedJob.analysis)[0]; + const actualType = Object.keys(sortedActualJobs[i].analysis)[0]; + expect(actualType).to.eql( + expectedType, + `Expected job type to be '${expectedType}' (got '${actualType}')` + ); + expect(sortedActualJobs[i].dest.index).to.eql( + expectedJob.dest.index, + `Expected destination index to be '${expectedJob.dest.index}' (got '${sortedActualJobs[i].dest.index}')` + ); + }); + }, }; } diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index 27da58972556d..5424d7068529e 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -167,7 +167,7 @@ export default function ({ getService }: FtrProviderContext) { const { body: { data, total }, - } = await findEvents(undefined, id, {}); + } = await findEventsByIds(undefined, [id], {}, [id]); expect(data.length).to.be(6); expect(total).to.be(6); @@ -184,6 +184,51 @@ export default function ({ getService }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/event_log_multiple_indicies'); }); }); + + describe(`Legacy Ids`, () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/event_log_legacy_ids'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/event_log_legacy_ids'); + }); + it('should support search event by ids and legacyIds', async () => { + const legacyId = `521f2511-5cd1-44fd-95df-e0df83e354d5`; + const id = `621f2511-5cd1-44fd-95df-e0df83e354d5`; + + const { + body: { data, total }, + } = await findEventsByIds(undefined, [id], {}, [legacyId]); + + expect(data.length).to.be(5); + expect(total).to.be(5); + + expect(data.map((foundEvent: IEvent) => foundEvent?.message)).to.eql([ + 'test 2020-10-28T15:19:55.913Z', + 'test legacy 2020-10-28T15:19:55.913Z', + 'test 2020-10-28T15:19:55.938Z', + 'test legacy 2020-10-28T15:19:55.962Z', + 'test 2020-10-28T15:19:55.962Z', + ]); + }); + + it('should search event only by ids if no legacyIds are provided', async () => { + const id = `621f2511-5cd1-44fd-95df-e0df83e354d5`; + + const { + body: { data, total }, + } = await findEventsByIds(undefined, [id], {}); + + expect(data.length).to.be(3); + expect(total).to.be(3); + + expect(data.map((foundEvent: IEvent) => foundEvent?.message)).to.eql([ + 'test 2020-10-28T15:19:55.913Z', + 'test 2020-10-28T15:19:55.938Z', + 'test 2020-10-28T15:19:55.962Z', + ]); + }); + }); }); async function findEvents( @@ -204,6 +249,32 @@ export default function ({ getService }: FtrProviderContext) { return await supertest.get(url).set('kbn-xsrf', 'foo').expect(200); } + async function findEventsByIds( + namespace: string | undefined, + ids: string[], + query: Record = {}, + legacyIds: string[] = [] + ) { + const urlPrefix = urlPrefixFromNamespace(namespace); + const url = `${urlPrefix}/api/event_log/event_log_test/_find${ + isEmpty(query) + ? '' + : `?${Object.entries(query) + .map(([key, val]) => `${key}=${val}`) + .join('&')}` + }`; + await delay(1000); // wait for buffer to be written + log.debug(`Finding Events for Saved Object with ${url}`); + return await supertest + .post(url) + .set('kbn-xsrf', 'foo') + .send({ + ids, + legacyIds, + }) + .expect(200); + } + function assertEventsFromApiMatchCreatedEvents( foundEvents: IValidatedEvent[], expectedEvents: IEvent[] diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx index adc10ae0a4161..a37c00144504d 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -11,6 +11,7 @@ import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public'; import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; @@ -60,39 +61,41 @@ const AppRoot = React.memo( - {(timelinesPluginSetup && - timelinesPluginSetup.getTGrid && - timelinesPluginSetup.getTGrid<'standalone'>({ - appId: 'securitySolution', - type: 'standalone', - casePermissions: { - read: true, - crud: true, - }, - columns: [], - indexNames: [], - deletedEventIds: [], - end: '', - footerText: 'Events', - filters: [], - hasAlertsCrudPermissions, - itemsPerPageOptions: [1, 2, 3], - loadingText: 'Loading events', - renderCellValue: () =>
test
, - sort: [], - leadingControlColumns: [], - trailingControlColumns: [], - query: { - query: '', - language: 'kuery', - }, - setRefetch, - start: '', - rowRenderers: [], - filterStatus: 'open', - unit: (n: number) => `${n}`, - })) ?? - null} + + {(timelinesPluginSetup && + timelinesPluginSetup.getTGrid && + timelinesPluginSetup.getTGrid<'standalone'>({ + appId: 'securitySolution', + type: 'standalone', + casePermissions: { + read: true, + crud: true, + }, + columns: [], + indexNames: [], + deletedEventIds: [], + end: '', + footerText: 'Events', + filters: [], + hasAlertsCrudPermissions, + itemsPerPageOptions: [1, 2, 3], + loadingText: 'Loading events', + renderCellValue: () =>
test
, + sort: [], + leadingControlColumns: [], + trailingControlColumns: [], + query: { + query: '', + language: 'kuery', + }, + setRefetch, + start: '', + rowRenderers: [], + filterStatus: 'open', + unit: (n: number) => `${n}`, + })) ?? + null} +
diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts index b312ba6769272..fd49e2b237217 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts @@ -20,8 +20,7 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const security = getService('security'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/110483 - describe.skip('ILM policy migration APIs', () => { + describe('ILM policy migration APIs', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/logs'); await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/security_api_integration/oidc.config.ts b/x-pack/test/security_api_integration/oidc.config.ts index a475d77aa568b..b2822a49b2042 100644 --- a/x-pack/test/security_api_integration/oidc.config.ts +++ b/x-pack/test/security_api_integration/oidc.config.ts @@ -50,7 +50,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), `--plugin-path=${plugin}`, - `--xpack.security.authc.providers=${JSON.stringify(['oidc', 'basic'])}`, + `--xpack.security.authProviders=${JSON.stringify(['oidc', 'basic'])}`, '--xpack.security.authc.oidc.realm="oidc1"', ], },