diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index feaf47e45fd69..e707250ff3261 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -202,7 +202,8 @@ # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team /x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team -/x-pack/test/functional/apps/endpoint/ @elastic/endpoint-app-team +/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team +/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team /x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team # SIEM diff --git a/.i18nrc.json b/.i18nrc.json index 3b0b40b40792e..19d361aed9344 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -24,6 +24,7 @@ "src/legacy/core_plugins/management", "src/plugins/management" ], + "indexPatternManagement": "src/plugins/index_pattern_management", "advancedSettings": "src/plugins/advanced_settings", "kibana_legacy": "src/plugins/kibana_legacy", "kibana_react": "src/legacy/core_plugins/kibana_react", diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc new file mode 100644 index 0000000000000..b8552c007b13d --- /dev/null +++ b/docs/apm/apm-alerts.asciidoc @@ -0,0 +1,97 @@ +[role="xpack"] +[[apm-alerts]] +=== Create an alert + +beta::[] + +The APM app is integrated with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature. +It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use, +and allows all alerts to be centrally managed from <>. + +[role="screenshot"] +image::apm/images/apm-alert.png[Create an alert in the APM app] + +There are two different types of threshold alerts: transaction duration, and error rate. +Below, we'll create one of each. + +[float] +[[apm-create-transaction-alert]] +=== Create a transaction duration alert + +This guide creates an alert for the `opbeans-java` service based on the following criteria: + +* Transaction type: `transaction.type:request` +* Average request is above `1500ms` for the last 5 minutes +* Check every 10 minutes, and repeat the alert every 30 minutes +* Send the alert via Slack + +From the APM app, navigate to the `opbeans-java` service and select +**Alerts** > **Create threshold alert** > **Transaction duration**. + +The name of your alert will automatically be set as `Transaction duration | opbeans-java`, +and the alert will be tagged with `apm` and `service.name:opbeans-java`. +Feel free to edit either of these defaults. + +Based on the alert criteria, define the following alert details: + +* **Check every** - `10 minutes` +* **Notify every** - `30 minutes` +* **TYPE** - `request` +* **WHEN** - `avg` +* **IS ABOVE** - `1500ms` +* **FOR THE LAST** - `5 minutes` + +Select an action type. +Multiple action types can be selected, but in this example we want to post to a slack channel. +Select **Slack** > **Create a connector**. +Enter a name for the connector, +and paste the webhook URL. +See Slack's webhook documentation if you need to create one. + +Select **Save**. The alert has been created and is now active! + +[float] +[[apm-create-error-alert]] +=== Create an error rate alert + +This guide creates an alert for the `opbeans-python` service based on the following criteria: + +* Error rate is above 25 for the last minute +* Check every 1 minute, and repeat the alert every 10 minutes +* Send the alert via email to the `opbeans-python` team + +From the APM app, navigate to the `opbeans-python` service and select +**Alerts** > **Create threshold alert** > **Error rate**. + +The name of your alert will automatically be set as `Error rate | opbeans-python`, +and the alert will be tagged with `apm` and `service.name:opbeans-python`. +Feel free to edit either of these defaults. + +Based on the alert criteria, define the following alert details: + +* **Check every** - `1 minute` +* **Notify every** - `10 minutes` +* **IS ABOVE** - `25 errors` +* **FOR THE LAST** - `1 minute` + +Select the **Email** action type and click **Create a connector**. +Fill out the required details: sender, host, port, etc., and click **save**. + +Select **Save**. The alert has been created and is now active! + +[float] +[[apm-alert-manage]] +=== Manage alerts and actions + +From the APM app, select **Alerts** > **View active alerts** to be taken to the Kibana alerts and actions management page. +From this page, you can create, edit, disable, mute, and delete alerts, and create, edit, and disable connectors. + +[float] +[[apm-alert-more-info]] +=== More information + +See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more information. + +NOTE: If you are using an **on-premise** Elastic Stack deployment with security, +TLS must be configured for communication between Elasticsearch and Kibana. +More information is in the alerting {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[prerequisites]. \ No newline at end of file diff --git a/docs/apm/images/apm-alert.png b/docs/apm/images/apm-alert.png new file mode 100644 index 0000000000000..4cee7214637f8 Binary files /dev/null and b/docs/apm/images/apm-alert.png differ diff --git a/docs/apm/using-the-apm-ui.asciidoc b/docs/apm/using-the-apm-ui.asciidoc index b1b7ed7307986..904718999069d 100644 --- a/docs/apm/using-the-apm-ui.asciidoc +++ b/docs/apm/using-the-apm-ui.asciidoc @@ -15,6 +15,7 @@ APM is available via the navigation sidebar in {Kib}. * <> * <> * <> +* <> * <> * <> * <> @@ -37,6 +38,8 @@ include::errors.asciidoc[] include::metrics.asciidoc[] +include::apm-alerts.asciidoc[] + include::agent-configuration.asciidoc[] include::custom-links.asciidoc[] diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 29fdc37a81176..c10b460da8b4f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -23,6 +23,7 @@ export interface CoreSetupHttpServiceSetup | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | +| [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | | [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | | [uuid](./kibana-plugin-core-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-core-server.uuidservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.status.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.status.md new file mode 100644 index 0000000000000..f5ea627a9f008 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.status.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [status](./kibana-plugin-core-server.coresetup.status.md) + +## CoreSetup.status property + +[StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) + +Signature: + +```typescript +status: StatusServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestatus.elasticsearch.md b/docs/development/core/server/kibana-plugin-core-server.corestatus.elasticsearch.md new file mode 100644 index 0000000000000..b41e7020c38e9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestatus.elasticsearch.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStatus](./kibana-plugin-core-server.corestatus.md) > [elasticsearch](./kibana-plugin-core-server.corestatus.elasticsearch.md) + +## CoreStatus.elasticsearch property + +Signature: + +```typescript +elasticsearch: ServiceStatus; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestatus.md b/docs/development/core/server/kibana-plugin-core-server.corestatus.md new file mode 100644 index 0000000000000..3fde86a18c58b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestatus.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStatus](./kibana-plugin-core-server.corestatus.md) + +## CoreStatus interface + +Status of core services. + +Signature: + +```typescript +export interface CoreStatus +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [elasticsearch](./kibana-plugin-core-server.corestatus.elasticsearch.md) | ServiceStatus | | +| [savedObjects](./kibana-plugin-core-server.corestatus.savedobjects.md) | ServiceStatus | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.corestatus.savedobjects.md b/docs/development/core/server/kibana-plugin-core-server.corestatus.savedobjects.md new file mode 100644 index 0000000000000..d554c6f70d720 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestatus.savedobjects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStatus](./kibana-plugin-core-server.corestatus.md) > [savedObjects](./kibana-plugin-core-server.corestatus.savedobjects.md) + +## CoreStatus.savedObjects property + +Signature: + +```typescript +savedObjects: ServiceStatus; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md new file mode 100644 index 0000000000000..f8a45fe9a5a9c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) > [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) + +## ElasticsearchStatusMeta.incompatibleNodes property + +Signature: + +```typescript +incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md new file mode 100644 index 0000000000000..2398410fa4b84 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) + +## ElasticsearchStatusMeta interface + + +Signature: + +```typescript +export interface ElasticsearchStatusMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) | NodesVersionCompatibility['incompatibleNodes'] | | +| [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) | NodesVersionCompatibility['warningNodes'] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md new file mode 100644 index 0000000000000..7374ccd9e7fa8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) > [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) + +## ElasticsearchStatusMeta.warningNodes property + +Signature: + +```typescript +warningNodes: NodesVersionCompatibility['warningNodes']; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 793684c1b3796..accab9bf0cb36 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -66,6 +66,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | | [CoreSetup](./kibana-plugin-core-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-core-server.corestart.md) | Context passed to the plugins start method. | +| [CoreStatus](./kibana-plugin-core-server.corestatus.md) | Status of core services. | | [CustomHttpResponseOptions](./kibana-plugin-core-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. | | [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) | | | [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) | | @@ -75,6 +76,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ElasticsearchError](./kibana-plugin-core-server.elasticsearcherror.md) | | | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | +| [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) | | | [EnvironmentMode](./kibana-plugin-core-server.environmentmode.md) | | | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | @@ -101,6 +103,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LoggerFactory](./kibana-plugin-core-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-core-server.logmeta.md) | Contextual metadata | | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | +| [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) | | | [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-core-server.onpreresponseextensions.md) | Additional data to extend a response. | @@ -162,15 +165,18 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | Saved Objects is Kibana's data persistence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for registering Saved Object types, creating and registering Saved Object client wrappers and factories. | | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceStart API provides a scoped Saved Objects client for interacting with Saved Objects. | +| [SavedObjectStatusMeta](./kibana-plugin-core-server.savedobjectstatusmeta.md) | Meta information about the SavedObjectService's status. Available to plugins via [CoreSetup.status](./kibana-plugin-core-server.coresetup.status.md). | | [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) | | | [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) | Configuration options for the [type](./kibana-plugin-core-server.savedobjectstype.md)'s management section. | | [SavedObjectsTypeMappingDefinition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. | | [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-core-server.savedobjectsupdateresponse.md) | | +| [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) | The current status of a service at a point in time. | | [SessionCookieValidationResult](./kibana-plugin-core-server.sessioncookievalidationresult.md) | Return type from a function to validate cookie contents. | | [SessionStorage](./kibana-plugin-core-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | | [SessionStorageCookieOptions](./kibana-plugin-core-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. | | [SessionStorageFactory](./kibana-plugin-core-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | +| [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. | | [StringValidationRegex](./kibana-plugin-core-server.stringvalidationregex.md) | StringValidation with regex object | | [StringValidationRegexString](./kibana-plugin-core-server.stringvalidationregexstring.md) | StringValidation as regex string | | [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. | @@ -184,6 +190,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Variable | Description | | --- | --- | | [kibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution. | +| [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) | The current "level" of availability of a service. | | [validBodyOutput](./kibana-plugin-core-server.validbodyoutput.md) | The set of valid body.output | ## Type Aliases @@ -256,6 +263,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | | [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md). | +| [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) | A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). | | [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | | | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed start. This should only be used inside handlers registered during setup that will only be executed after start lifecycle. | | [StringValidation](./kibana-plugin-core-server.stringvalidation.md) | Allows regex objects or a regex string | diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md new file mode 100644 index 0000000000000..8e7298d28801c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [incompatibleNodes](./kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md) + +## NodesVersionCompatibility.incompatibleNodes property + +Signature: + +```typescript +incompatibleNodes: NodeInfo[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md new file mode 100644 index 0000000000000..82a4800a3b4b6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) + +## NodesVersionCompatibility.isCompatible property + +Signature: + +```typescript +isCompatible: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md new file mode 100644 index 0000000000000..347f2d3474b11 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) + +## NodesVersionCompatibility.kibanaVersion property + +Signature: + +```typescript +kibanaVersion: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md new file mode 100644 index 0000000000000..6fcfacc3bc908 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) + +## NodesVersionCompatibility interface + +Signature: + +```typescript +export interface NodesVersionCompatibility +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [incompatibleNodes](./kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md) | NodeInfo[] | | +| [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) | boolean | | +| [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) | string | | +| [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) | string | | +| [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) | NodeInfo[] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.message.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.message.md new file mode 100644 index 0000000000000..415a7825ee2bf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) + +## NodesVersionCompatibility.message property + +Signature: + +```typescript +message?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md new file mode 100644 index 0000000000000..6c017e9fc800c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) + +## NodesVersionCompatibility.warningNodes property + +Signature: + +```typescript +warningNodes: NodeInfo[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md new file mode 100644 index 0000000000000..3a0b23d18632f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectStatusMeta](./kibana-plugin-core-server.savedobjectstatusmeta.md) + +## SavedObjectStatusMeta interface + +Meta information about the SavedObjectService's status. Available to plugins via [CoreSetup.status](./kibana-plugin-core-server.coresetup.status.md). + +Signature: + +```typescript +export interface SavedObjectStatusMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [migratedIndices](./kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md) | {
[status: string]: number;
skipped: number;
migrated: number;
} | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md new file mode 100644 index 0000000000000..6a29623b2f122 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectStatusMeta](./kibana-plugin-core-server.savedobjectstatusmeta.md) > [migratedIndices](./kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md) + +## SavedObjectStatusMeta.migratedIndices property + +Signature: + +```typescript +migratedIndices: { + [status: string]: number; + skipped: number; + migrated: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.detail.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.detail.md new file mode 100644 index 0000000000000..fa369aa0bdfbb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.detail.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [detail](./kibana-plugin-core-server.servicestatus.detail.md) + +## ServiceStatus.detail property + +A more detailed description of the service status. + +Signature: + +```typescript +detail?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.documentationurl.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.documentationurl.md new file mode 100644 index 0000000000000..5ef8c1251a602 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.documentationurl.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [documentationUrl](./kibana-plugin-core-server.servicestatus.documentationurl.md) + +## ServiceStatus.documentationUrl property + +A URL to open in a new tab about how to resolve or troubleshoot the problem. + +Signature: + +```typescript +documentationUrl?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.level.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.level.md new file mode 100644 index 0000000000000..551c10c9bff82 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.level.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [level](./kibana-plugin-core-server.servicestatus.level.md) + +## ServiceStatus.level property + +The current availability level of the service. + +Signature: + +```typescript +level: ServiceStatusLevel; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.md new file mode 100644 index 0000000000000..d35fc951c57ff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) + +## ServiceStatus interface + +The current status of a service at a point in time. + +Signature: + +```typescript +export interface ServiceStatus | unknown = unknown> +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [detail](./kibana-plugin-core-server.servicestatus.detail.md) | string | A more detailed description of the service status. | +| [documentationUrl](./kibana-plugin-core-server.servicestatus.documentationurl.md) | string | A URL to open in a new tab about how to resolve or troubleshoot the problem. | +| [level](./kibana-plugin-core-server.servicestatus.level.md) | ServiceStatusLevel | The current availability level of the service. | +| [meta](./kibana-plugin-core-server.servicestatus.meta.md) | Meta | Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, machine-readable information about the service status. May include status information for underlying features. | +| [summary](./kibana-plugin-core-server.servicestatus.summary.md) | string | A high-level summary of the service status. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.meta.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.meta.md new file mode 100644 index 0000000000000..a48994daa5a4e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.meta.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [meta](./kibana-plugin-core-server.servicestatus.meta.md) + +## ServiceStatus.meta property + +Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, machine-readable information about the service status. May include status information for underlying features. + +Signature: + +```typescript +meta?: Meta; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.summary.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.summary.md new file mode 100644 index 0000000000000..db90afd6f74a6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.summary.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [summary](./kibana-plugin-core-server.servicestatus.summary.md) + +## ServiceStatus.summary property + +A high-level summary of the service status. + +Signature: + +```typescript +summary: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatuslevel.md b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevel.md new file mode 100644 index 0000000000000..5f995ff5e13e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) + +## ServiceStatusLevel type + +A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). + +Signature: + +```typescript +export declare type ServiceStatusLevel = typeof ServiceStatusLevels[keyof typeof ServiceStatusLevels]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md new file mode 100644 index 0000000000000..a66cec78c736b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md @@ -0,0 +1,37 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) + +## ServiceStatusLevels variable + +The current "level" of availability of a service. + +Signature: + +```typescript +ServiceStatusLevels: Readonly<{ + available: Readonly<{ + toString: () => "available"; + valueOf: () => 0; + }>; + degraded: Readonly<{ + toString: () => "degraded"; + valueOf: () => 1; + }>; + unavailable: Readonly<{ + toString: () => "unavailable"; + valueOf: () => 2; + }>; + critical: Readonly<{ + toString: () => "critical"; + valueOf: () => 3; + }>; +}> +``` + +## Remarks + +The values implement `valueOf` to allow for easy comparisons between status levels with <, >, etc. Higher values represent higher severities. Note that the default `Array.prototype.sort` implementation does not correctly sort these values. + +A snapshot serializer is available in `src/core/server/test_utils` to ease testing of these values with Jest. + diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.core_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.core_.md new file mode 100644 index 0000000000000..6662e68b44d36 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.core_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) + +## StatusServiceSetup.core$ property + +Current status for all Core services. + +Signature: + +```typescript +core$: Observable; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md new file mode 100644 index 0000000000000..0551a217520ad --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) + +## StatusServiceSetup interface + +API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. + +Signature: + +```typescript +export interface StatusServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md new file mode 100644 index 0000000000000..5c5aa348eecdf --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) + +## createSearchSource variable + +Deserializes a json string and a set of referenced objects to a `SearchSource` instance. Use this method to re-create the search source serialized using `searchSource.serialize`. + +This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service + +Signature: + +```typescript +createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 6964c070097c5..fc0dab94a0f65 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -102,6 +102,7 @@ | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | +| [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) | Deserializes a json string and a set of referenced objects to a SearchSource instance. Use this method to re-create the search source serialized using searchSource.serialize.This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md index 8e1dbb6e2671d..5f2fc809a5590 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md @@ -38,6 +38,7 @@ export declare class SearchSource | [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {undefined\|searchSource} | | [getSearchRequestBody()](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) | | | | [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start | +| [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named kibanaSavedObjectMeta.searchSourceJSON.index and kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index.Using createSearchSource, the instance can be re-created. | | [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | | | [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | | | [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md new file mode 100644 index 0000000000000..52d25dec01dfd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [serialize](./kibana-plugin-plugins-data-public.searchsource.serialize.md) + +## SearchSource.serialize() method + +Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object. + +The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. + +Using `createSearchSource`, the instance can be re-created. + +Signature: + +```typescript +serialize(): { + searchSourceJSON: string; + references: SavedObjectReference[]; + }; +``` +Returns: + +`{ + searchSourceJSON: string; + references: SavedObjectReference[]; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 259d725b3bf0d..e756eb9b72905 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -23,7 +23,6 @@ | Function | Description | | --- | --- | | [getDefaultSearchParams(config)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | | -| [getTotalLoaded({ total, failed, successful })](./kibana-plugin-plugins-data-server.gettotalloaded.md) | | | [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally | | [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | | diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index b4f7e6af3d61c..c7cf1186a44be 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -163,7 +163,8 @@ If you are using an *on-premises* Elastic Stack deployment with <> * <> * <> diff --git a/package.json b/package.json index 4c5db5321c282..bd0fec3a5c116 100644 --- a/package.json +++ b/package.json @@ -376,7 +376,7 @@ "@types/recompose": "^0.30.6", "@types/redux-actions": "^2.6.1", "@types/request": "^2.48.2", - "@types/selenium-webdriver": "^4.0.5", + "@types/selenium-webdriver": "4.0.9", "@types/semver": "^5.5.0", "@types/sinon": "^7.0.13", "@types/strip-ansi": "^3.0.0", @@ -462,6 +462,7 @@ "load-grunt-config": "^3.0.1", "mocha": "^7.1.1", "mock-http-server": "1.3.0", + "ms-chromium-edge-driver": "^0.2.0", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", @@ -480,7 +481,7 @@ "react-textarea-autosize": "^7.1.2", "regenerate": "^1.4.0", "sass-lint": "^1.12.1", - "selenium-webdriver": "^4.0.0-alpha.5", + "selenium-webdriver": "^4.0.0-alpha.7", "simple-git": "1.116.0", "simplebar-react": "^2.1.0", "sinon": "^7.4.2", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b648004760d7c..b3e5a8c518682 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -32,6 +32,7 @@ "json-stable-stringify": "^1.0.1", "loader-utils": "^1.2.3", "node-sass": "^4.13.0", + "normalize-path": "^3.0.0", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "resolve-url-loader": "^3.1.1", diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index d52d89eebe2f1..4b4bb1282d939 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -57,6 +57,6 @@ OptimizerConfig { } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i bundle.id === p.id)) { + return; + } + + // ignore requests that don't include a /data/public, /kibana_react/public, or + // /kibana_utils/public segment as a cheap way to avoid doing path resolution + // for paths that couldn't possibly resolve to what we're looking for + const reqToStaticBundle = STATIC_BUNDLE_PLUGINS.some(p => + request.includes(`/${p.dirname}/public`) + ); + if (!reqToStaticBundle) { + return; + } + + // determine the most acurate resolution string we can without running full resolution + const rootRelative = normalizePath( + Path.relative(bundle.sourceRoot, Path.resolve(context, request)) + ); + for (const { id, dirname } of STATIC_BUNDLE_PLUGINS) { + if (rootRelative === `src/plugins/${dirname}/public`) { + return `__kbnBundles__['plugin/${id}']`; + } + } + + // import doesn't match a root public import + return undefined; +} + export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { const commonConfig: webpack.Configuration = { node: { fs: 'empty' }, @@ -63,7 +117,6 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { // When the entry point is loaded, assign it's exported `plugin` // value to a key on the global `__kbnBundles__` object. library: ['__kbnBundles__', `plugin/${bundle.id}`], - libraryExport: 'plugin', } : {}), }, @@ -72,9 +125,16 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { noEmitOnErrors: true, }, - externals: { - ...UiSharedDeps.externals, - }, + externals: [ + UiSharedDeps.externals, + function(context, request, cb) { + try { + cb(undefined, dynamicExternals(bundle, context, request)); + } catch (error) { + cb(error, undefined); + } + }, + ], plugins: [new CleanWebpackPlugin(), new DisallowedSyntaxPlugin()], diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 0cc1ad6326671..7a858deff41d3 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -79260,7 +79260,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(928); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(923); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79445,9 +79445,9 @@ const pAll = __webpack_require__(707); const arrify = __webpack_require__(709); const globby = __webpack_require__(710); const isGlob = __webpack_require__(604); -const cpFile = __webpack_require__(913); -const junk = __webpack_require__(925); -const CpyError = __webpack_require__(926); +const cpFile = __webpack_require__(908); +const junk = __webpack_require__(920); +const CpyError = __webpack_require__(921); const defaultOptions = { ignoreJunk: true @@ -79697,8 +79697,8 @@ const fs = __webpack_require__(23); const arrayUnion = __webpack_require__(711); const glob = __webpack_require__(713); const fastGlob = __webpack_require__(718); -const dirGlob = __webpack_require__(906); -const gitignore = __webpack_require__(909); +const dirGlob = __webpack_require__(901); +const gitignore = __webpack_require__(904); const DEFAULT_FILTER = () => false; @@ -81531,11 +81531,11 @@ module.exports.generateTasks = pkg.generateTasks; Object.defineProperty(exports, "__esModule", { value: true }); var optionsManager = __webpack_require__(720); var taskManager = __webpack_require__(721); -var reader_async_1 = __webpack_require__(877); -var reader_stream_1 = __webpack_require__(901); -var reader_sync_1 = __webpack_require__(902); -var arrayUtils = __webpack_require__(904); -var streamUtils = __webpack_require__(905); +var reader_async_1 = __webpack_require__(872); +var reader_stream_1 = __webpack_require__(896); +var reader_sync_1 = __webpack_require__(897); +var arrayUtils = __webpack_require__(899); +var streamUtils = __webpack_require__(900); /** * Synchronous API. */ @@ -82175,9 +82175,9 @@ var extend = __webpack_require__(838); */ var compilers = __webpack_require__(841); -var parsers = __webpack_require__(873); -var cache = __webpack_require__(874); -var utils = __webpack_require__(875); +var parsers = __webpack_require__(868); +var cache = __webpack_require__(869); +var utils = __webpack_require__(870); var MAX_LENGTH = 1024 * 64; /** @@ -100710,9 +100710,9 @@ var toRegex = __webpack_require__(729); */ var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); -var Extglob = __webpack_require__(872); -var utils = __webpack_require__(871); +var parsers = __webpack_require__(864); +var Extglob = __webpack_require__(867); +var utils = __webpack_require__(866); var MAX_LENGTH = 1024 * 64; /** @@ -101222,7 +101222,7 @@ var parsers = __webpack_require__(862); * Module dependencies */ -var debug = __webpack_require__(864)('expand-brackets'); +var debug = __webpack_require__(801)('expand-brackets'); var extend = __webpack_require__(738); var Snapdragon = __webpack_require__(768); var toRegex = __webpack_require__(729); @@ -101816,839 +101816,12 @@ exports.createRegex = function(pattern, include) { /* 864 */ /***/ (function(module, exports, __webpack_require__) { -/** - * Detect Electron renderer process, which is node, but we should - * treat as a browser. - */ - -if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(865); -} else { - module.exports = __webpack_require__(868); -} - - -/***/ }), -/* 865 */ -/***/ (function(module, exports, __webpack_require__) { - -/** - * This is the web browser implementation of `debug()`. - * - * Expose `debug()` as the module. - */ - -exports = module.exports = __webpack_require__(866); -exports.log = log; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; -exports.storage = 'undefined' != typeof chrome - && 'undefined' != typeof chrome.storage - ? chrome.storage.local - : localstorage(); - -/** - * Colors. - */ - -exports.colors = [ - 'lightseagreen', - 'forestgreen', - 'goldenrod', - 'dodgerblue', - 'darkorchid', - 'crimson' -]; - -/** - * Currently only WebKit-based Web Inspectors, Firefox >= v31, - * and the Firebug extension (any Firefox version) are known - * to support "%c" CSS customizations. - * - * TODO: add a `localStorage` variable to explicitly enable/disable colors - */ - -function useColors() { - // NB: In an Electron preload script, document will be defined but not fully - // initialized. Since we know we're in Chrome, we'll just detect this case - // explicitly - if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { - return true; - } - - // is webkit? http://stackoverflow.com/a/16459606/376773 - // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 - return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || - // is firebug? http://stackoverflow.com/a/398120/376773 - (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || - // is firefox >= v31? - // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || - // double check webkit in userAgent just in case we are in a worker - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); -} - -/** - * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. - */ - -exports.formatters.j = function(v) { - try { - return JSON.stringify(v); - } catch (err) { - return '[UnexpectedJSONParseError]: ' + err.message; - } -}; - - -/** - * Colorize log arguments if enabled. - * - * @api public - */ - -function formatArgs(args) { - var useColors = this.useColors; - - args[0] = (useColors ? '%c' : '') - + this.namespace - + (useColors ? ' %c' : ' ') - + args[0] - + (useColors ? '%c ' : ' ') - + '+' + exports.humanize(this.diff); - - if (!useColors) return; - - var c = 'color: ' + this.color; - args.splice(1, 0, c, 'color: inherit') - - // the final "%c" is somewhat tricky, because there could be other - // arguments passed either before or after the %c, so we need to - // figure out the correct index to insert the CSS into - var index = 0; - var lastC = 0; - args[0].replace(/%[a-zA-Z%]/g, function(match) { - if ('%%' === match) return; - index++; - if ('%c' === match) { - // we only are interested in the *last* %c - // (the user may have provided their own) - lastC = index; - } - }); - - args.splice(lastC, 0, c); -} - -/** - * Invokes `console.log()` when available. - * No-op when `console.log` is not a "function". - * - * @api public - */ - -function log() { - // this hackery is required for IE8/9, where - // the `console.log` function doesn't have 'apply' - return 'object' === typeof console - && console.log - && Function.prototype.apply.call(console.log, console, arguments); -} - -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ - -function save(namespaces) { - try { - if (null == namespaces) { - exports.storage.removeItem('debug'); - } else { - exports.storage.debug = namespaces; - } - } catch(e) {} -} - -/** - * Load `namespaces`. - * - * @return {String} returns the previously persisted debug modes - * @api private - */ - -function load() { - var r; - try { - r = exports.storage.debug; - } catch(e) {} - - // If debug isn't set in LS, and we're in Electron, try to load $DEBUG - if (!r && typeof process !== 'undefined' && 'env' in process) { - r = process.env.DEBUG; - } - - return r; -} - -/** - * Enable namespaces listed in `localStorage.debug` initially. - */ - -exports.enable(load()); - -/** - * Localstorage attempts to return the localstorage. - * - * This is necessary because safari throws - * when a user disables cookies/localstorage - * and you attempt to access it. - * - * @return {LocalStorage} - * @api private - */ - -function localstorage() { - try { - return window.localStorage; - } catch (e) {} -} - - -/***/ }), -/* 866 */ -/***/ (function(module, exports, __webpack_require__) { - - -/** - * This is the common logic for both the Node.js and web browser - * implementations of `debug()`. - * - * Expose `debug()` as the module. - */ - -exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; -exports.coerce = coerce; -exports.disable = disable; -exports.enable = enable; -exports.enabled = enabled; -exports.humanize = __webpack_require__(867); - -/** - * The currently active debug mode names, and names to skip. - */ - -exports.names = []; -exports.skips = []; - -/** - * Map of special "%n" handling functions, for the debug "format" argument. - * - * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". - */ - -exports.formatters = {}; - -/** - * Previous log timestamp. - */ - -var prevTime; - -/** - * Select a color. - * @param {String} namespace - * @return {Number} - * @api private - */ - -function selectColor(namespace) { - var hash = 0, i; - - for (i in namespace) { - hash = ((hash << 5) - hash) + namespace.charCodeAt(i); - hash |= 0; // Convert to 32bit integer - } - - return exports.colors[Math.abs(hash) % exports.colors.length]; -} - -/** - * Create a debugger with the given `namespace`. - * - * @param {String} namespace - * @return {Function} - * @api public - */ - -function createDebug(namespace) { - - function debug() { - // disabled? - if (!debug.enabled) return; - - var self = debug; - - // set `diff` timestamp - var curr = +new Date(); - var ms = curr - (prevTime || curr); - self.diff = ms; - self.prev = prevTime; - self.curr = curr; - prevTime = curr; - - // turn the `arguments` into a proper Array - var args = new Array(arguments.length); - for (var i = 0; i < args.length; i++) { - args[i] = arguments[i]; - } - - args[0] = exports.coerce(args[0]); - - if ('string' !== typeof args[0]) { - // anything else let's inspect with %O - args.unshift('%O'); - } - - // apply any `formatters` transformations - var index = 0; - args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { - // if we encounter an escaped % then don't increase the array index - if (match === '%%') return match; - index++; - var formatter = exports.formatters[format]; - if ('function' === typeof formatter) { - var val = args[index]; - match = formatter.call(self, val); - - // now we need to remove `args[index]` since it's inlined in the `format` - args.splice(index, 1); - index--; - } - return match; - }); - - // apply env-specific formatting (colors, etc.) - exports.formatArgs.call(self, args); - - var logFn = debug.log || exports.log || console.log.bind(console); - logFn.apply(self, args); - } - - debug.namespace = namespace; - debug.enabled = exports.enabled(namespace); - debug.useColors = exports.useColors(); - debug.color = selectColor(namespace); - - // env-specific initialization logic for debug instances - if ('function' === typeof exports.init) { - exports.init(debug); - } - - return debug; -} - -/** - * Enables a debug mode by namespaces. This can include modes - * separated by a colon and wildcards. - * - * @param {String} namespaces - * @api public - */ - -function enable(namespaces) { - exports.save(namespaces); - - exports.names = []; - exports.skips = []; - - var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); - var len = split.length; - - for (var i = 0; i < len; i++) { - if (!split[i]) continue; // ignore empty strings - namespaces = split[i].replace(/\*/g, '.*?'); - if (namespaces[0] === '-') { - exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); - } else { - exports.names.push(new RegExp('^' + namespaces + '$')); - } - } -} - -/** - * Disable debug output. - * - * @api public - */ - -function disable() { - exports.enable(''); -} - -/** - * Returns true if the given mode name is enabled, false otherwise. - * - * @param {String} name - * @return {Boolean} - * @api public - */ - -function enabled(name) { - var i, len; - for (i = 0, len = exports.skips.length; i < len; i++) { - if (exports.skips[i].test(name)) { - return false; - } - } - for (i = 0, len = exports.names.length; i < len; i++) { - if (exports.names[i].test(name)) { - return true; - } - } - return false; -} - -/** - * Coerce `val`. - * - * @param {Mixed} val - * @return {Mixed} - * @api private - */ - -function coerce(val) { - if (val instanceof Error) return val.stack || val.message; - return val; -} - - -/***/ }), -/* 867 */ -/***/ (function(module, exports) { - -/** - * Helpers. - */ - -var s = 1000; -var m = s * 60; -var h = m * 60; -var d = h * 24; -var y = d * 365.25; - -/** - * Parse or format the given `val`. - * - * Options: - * - * - `long` verbose formatting [false] - * - * @param {String|Number} val - * @param {Object} [options] - * @throws {Error} throw an error if val is not a non-empty string or a number - * @return {String|Number} - * @api public - */ - -module.exports = function(val, options) { - options = options || {}; - var type = typeof val; - if (type === 'string' && val.length > 0) { - return parse(val); - } else if (type === 'number' && isNaN(val) === false) { - return options.long ? fmtLong(val) : fmtShort(val); - } - throw new Error( - 'val is not a non-empty string or a valid number. val=' + - JSON.stringify(val) - ); -}; - -/** - * Parse the given `str` and return milliseconds. - * - * @param {String} str - * @return {Number} - * @api private - */ - -function parse(str) { - str = String(str); - if (str.length > 100) { - return; - } - var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( - str - ); - if (!match) { - return; - } - var n = parseFloat(match[1]); - var type = (match[2] || 'ms').toLowerCase(); - switch (type) { - case 'years': - case 'year': - case 'yrs': - case 'yr': - case 'y': - return n * y; - case 'days': - case 'day': - case 'd': - return n * d; - case 'hours': - case 'hour': - case 'hrs': - case 'hr': - case 'h': - return n * h; - case 'minutes': - case 'minute': - case 'mins': - case 'min': - case 'm': - return n * m; - case 'seconds': - case 'second': - case 'secs': - case 'sec': - case 's': - return n * s; - case 'milliseconds': - case 'millisecond': - case 'msecs': - case 'msec': - case 'ms': - return n; - default: - return undefined; - } -} - -/** - * Short format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ - -function fmtShort(ms) { - if (ms >= d) { - return Math.round(ms / d) + 'd'; - } - if (ms >= h) { - return Math.round(ms / h) + 'h'; - } - if (ms >= m) { - return Math.round(ms / m) + 'm'; - } - if (ms >= s) { - return Math.round(ms / s) + 's'; - } - return ms + 'ms'; -} - -/** - * Long format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ - -function fmtLong(ms) { - return plural(ms, d, 'day') || - plural(ms, h, 'hour') || - plural(ms, m, 'minute') || - plural(ms, s, 'second') || - ms + ' ms'; -} - -/** - * Pluralization helper. - */ - -function plural(ms, n, name) { - if (ms < n) { - return; - } - if (ms < n * 1.5) { - return Math.floor(ms / n) + ' ' + name; - } - return Math.ceil(ms / n) + ' ' + name + 's'; -} - - -/***/ }), -/* 868 */ -/***/ (function(module, exports, __webpack_require__) { - -/** - * Module dependencies. - */ - -var tty = __webpack_require__(478); -var util = __webpack_require__(29); - -/** - * This is the Node.js implementation of `debug()`. - * - * Expose `debug()` as the module. - */ - -exports = module.exports = __webpack_require__(866); -exports.init = init; -exports.log = log; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; - -/** - * Colors. - */ - -exports.colors = [6, 2, 3, 4, 5, 1]; - -/** - * Build up the default `inspectOpts` object from the environment variables. - * - * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js - */ - -exports.inspectOpts = Object.keys(process.env).filter(function (key) { - return /^debug_/i.test(key); -}).reduce(function (obj, key) { - // camel-case - var prop = key - .substring(6) - .toLowerCase() - .replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); - - // coerce string value into JS value - var val = process.env[key]; - if (/^(yes|on|true|enabled)$/i.test(val)) val = true; - else if (/^(no|off|false|disabled)$/i.test(val)) val = false; - else if (val === 'null') val = null; - else val = Number(val); - - obj[prop] = val; - return obj; -}, {}); - -/** - * The file descriptor to write the `debug()` calls to. - * Set the `DEBUG_FD` env variable to override with another value. i.e.: - * - * $ DEBUG_FD=3 node script.js 3>debug.log - */ - -var fd = parseInt(process.env.DEBUG_FD, 10) || 2; - -if (1 !== fd && 2 !== fd) { - util.deprecate(function(){}, 'except for stderr(2) and stdout(1), any other usage of DEBUG_FD is deprecated. Override debug.log if you want to use a different log function (https://git.io/debug_fd)')() -} - -var stream = 1 === fd ? process.stdout : - 2 === fd ? process.stderr : - createWritableStdioStream(fd); - -/** - * Is stdout a TTY? Colored output is enabled when `true`. - */ - -function useColors() { - return 'colors' in exports.inspectOpts - ? Boolean(exports.inspectOpts.colors) - : tty.isatty(fd); -} - -/** - * Map %o to `util.inspect()`, all on a single line. - */ - -exports.formatters.o = function(v) { - this.inspectOpts.colors = this.useColors; - return util.inspect(v, this.inspectOpts) - .split('\n').map(function(str) { - return str.trim() - }).join(' '); -}; - -/** - * Map %o to `util.inspect()`, allowing multiple lines if needed. - */ - -exports.formatters.O = function(v) { - this.inspectOpts.colors = this.useColors; - return util.inspect(v, this.inspectOpts); -}; - -/** - * Adds ANSI color escape codes if enabled. - * - * @api public - */ - -function formatArgs(args) { - var name = this.namespace; - var useColors = this.useColors; - - if (useColors) { - var c = this.color; - var prefix = ' \u001b[3' + c + ';1m' + name + ' ' + '\u001b[0m'; - - args[0] = prefix + args[0].split('\n').join('\n' + prefix); - args.push('\u001b[3' + c + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); - } else { - args[0] = new Date().toUTCString() - + ' ' + name + ' ' + args[0]; - } -} - -/** - * Invokes `util.format()` with the specified arguments and writes to `stream`. - */ - -function log() { - return stream.write(util.format.apply(util, arguments) + '\n'); -} - -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ - -function save(namespaces) { - if (null == namespaces) { - // If you set a process.env field to null or undefined, it gets cast to the - // string 'null' or 'undefined'. Just delete instead. - delete process.env.DEBUG; - } else { - process.env.DEBUG = namespaces; - } -} - -/** - * Load `namespaces`. - * - * @return {String} returns the previously persisted debug modes - * @api private - */ - -function load() { - return process.env.DEBUG; -} - -/** - * Copied from `node/src/node.js`. - * - * XXX: It's lame that node doesn't expose this API out-of-the-box. It also - * relies on the undocumented `tty_wrap.guessHandleType()` which is also lame. - */ - -function createWritableStdioStream (fd) { - var stream; - var tty_wrap = process.binding('tty_wrap'); - - // Note stream._type is used for test-module-load-list.js - - switch (tty_wrap.guessHandleType(fd)) { - case 'TTY': - stream = new tty.WriteStream(fd); - stream._type = 'tty'; - - // Hack to have stream not keep the event loop alive. - // See https://github.com/joyent/node/issues/1726 - if (stream._handle && stream._handle.unref) { - stream._handle.unref(); - } - break; - - case 'FILE': - var fs = __webpack_require__(23); - stream = new fs.SyncWriteStream(fd, { autoClose: false }); - stream._type = 'fs'; - break; - - case 'PIPE': - case 'TCP': - var net = __webpack_require__(806); - stream = new net.Socket({ - fd: fd, - readable: false, - writable: true - }); - - // FIXME Should probably have an option in net.Socket to create a - // stream from an existing fd which is writable only. But for now - // we'll just add this hack and set the `readable` member to false. - // Test: ./node test/fixtures/echo.js < /etc/passwd - stream.readable = false; - stream.read = null; - stream._type = 'pipe'; - - // FIXME Hack to have stream not keep the event loop alive. - // See https://github.com/joyent/node/issues/1726 - if (stream._handle && stream._handle.unref) { - stream._handle.unref(); - } - break; - - default: - // Probably an error on in uv_guess_handle() - throw new Error('Implement me. Unknown stream file type!'); - } - - // For supporting legacy API we put the FD here. - stream.fd = fd; - - stream._isStdio = true; - - return stream; -} - -/** - * Init logic for `debug` instances. - * - * Create a new `inspectOpts` object in case `useColors` is set - * differently for a particular `debug` instance. - */ - -function init (debug) { - debug.inspectOpts = {}; - - var keys = Object.keys(exports.inspectOpts); - for (var i = 0; i < keys.length; i++) { - debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; - } -} - -/** - * Enable namespaces listed in `process.env.DEBUG` initially. - */ - -exports.enable(load()); - - -/***/ }), -/* 869 */ -/***/ (function(module, exports, __webpack_require__) { - "use strict"; var brackets = __webpack_require__(859); -var define = __webpack_require__(870); -var utils = __webpack_require__(871); +var define = __webpack_require__(865); +var utils = __webpack_require__(866); /** * Characters to use in text regex (we want to "not" match @@ -102803,7 +101976,7 @@ module.exports = parsers; /***/ }), -/* 870 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102841,7 +102014,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 871 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102917,7 +102090,7 @@ utils.createRegex = function(str) { /***/ }), -/* 872 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102928,7 +102101,7 @@ utils.createRegex = function(str) { */ var Snapdragon = __webpack_require__(768); -var define = __webpack_require__(870); +var define = __webpack_require__(865); var extend = __webpack_require__(738); /** @@ -102936,7 +102109,7 @@ var extend = __webpack_require__(738); */ var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); +var parsers = __webpack_require__(864); /** * Customize Snapdragon parser and renderer @@ -103002,7 +102175,7 @@ module.exports = Extglob; /***/ }), -/* 873 */ +/* 868 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103092,14 +102265,14 @@ function textRegex(pattern) { /***/ }), -/* 874 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { module.exports = new (__webpack_require__(850))(); /***/ }), -/* 875 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103117,7 +102290,7 @@ utils.define = __webpack_require__(837); utils.diff = __webpack_require__(854); utils.extend = __webpack_require__(838); utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(876); +utils.typeOf = __webpack_require__(871); utils.unique = __webpack_require__(741); /** @@ -103415,7 +102588,7 @@ utils.unixify = function(options) { /***/ }), -/* 876 */ +/* 871 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -103550,7 +102723,7 @@ function isBuffer(val) { /***/ }), -/* 877 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103569,9 +102742,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(873); +var reader_1 = __webpack_require__(886); +var fs_stream_1 = __webpack_require__(890); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -103632,15 +102805,15 @@ exports.default = ReaderAsync; /***/ }), -/* 878 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(879); -const readdirAsync = __webpack_require__(887); -const readdirStream = __webpack_require__(890); +const readdirSync = __webpack_require__(874); +const readdirAsync = __webpack_require__(882); +const readdirStream = __webpack_require__(885); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -103724,7 +102897,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 879 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103732,11 +102905,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(875); let syncFacade = { - fs: __webpack_require__(885), - forEach: __webpack_require__(886), + fs: __webpack_require__(880), + forEach: __webpack_require__(881), sync: true }; @@ -103765,7 +102938,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 880 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103774,9 +102947,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(881); -const stat = __webpack_require__(883); -const call = __webpack_require__(884); +const normalizeOptions = __webpack_require__(876); +const stat = __webpack_require__(878); +const call = __webpack_require__(879); /** * Asynchronously reads the contents of a directory and streams the results @@ -104152,14 +103325,14 @@ module.exports = DirectoryReader; /***/ }), -/* 881 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(882); +const globToRegExp = __webpack_require__(877); module.exports = normalizeOptions; @@ -104336,7 +103509,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 882 */ +/* 877 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -104473,13 +103646,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 883 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(884); +const call = __webpack_require__(879); module.exports = stat; @@ -104554,7 +103727,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 884 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104615,14 +103788,14 @@ function callOnce (fn) { /***/ }), -/* 885 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(884); +const call = __webpack_require__(879); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -104686,7 +103859,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 886 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104715,7 +103888,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 887 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104723,12 +103896,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(888); -const DirectoryReader = __webpack_require__(880); +const maybe = __webpack_require__(883); +const DirectoryReader = __webpack_require__(875); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(884), async: true }; @@ -104770,7 +103943,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 888 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104797,7 +103970,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 889 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104833,7 +104006,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 890 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104841,11 +104014,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(875); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(884), async: true }; @@ -104865,16 +104038,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 891 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(892); -var entry_1 = __webpack_require__(894); -var pathUtil = __webpack_require__(893); +var deep_1 = __webpack_require__(887); +var entry_1 = __webpack_require__(889); +var pathUtil = __webpack_require__(888); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -104940,13 +104113,13 @@ exports.default = Reader; /***/ }), -/* 892 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); +var pathUtils = __webpack_require__(888); var patternUtils = __webpack_require__(722); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { @@ -105030,7 +104203,7 @@ exports.default = DeepFilter; /***/ }), -/* 893 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105061,13 +104234,13 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 894 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); +var pathUtils = __webpack_require__(888); var patternUtils = __webpack_require__(722); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { @@ -105153,7 +104326,7 @@ exports.default = EntryFilter; /***/ }), -/* 895 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105173,8 +104346,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(891); +var fs_1 = __webpack_require__(895); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -105224,14 +104397,14 @@ exports.default = FileSystemStream; /***/ }), -/* 896 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(897); -const statProvider = __webpack_require__(899); +const optionsManager = __webpack_require__(892); +const statProvider = __webpack_require__(894); /** * Asynchronous API. */ @@ -105262,13 +104435,13 @@ exports.statSync = statSync; /***/ }), -/* 897 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(898); +const fsAdapter = __webpack_require__(893); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -105281,7 +104454,7 @@ exports.prepare = prepare; /***/ }), -/* 898 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105304,7 +104477,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 899 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105356,7 +104529,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 900 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105387,7 +104560,7 @@ exports.default = FileSystem; /***/ }), -/* 901 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105407,9 +104580,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(873); +var reader_1 = __webpack_require__(886); +var fs_stream_1 = __webpack_require__(890); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -105477,7 +104650,7 @@ exports.default = ReaderStream; /***/ }), -/* 902 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105496,9 +104669,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_sync_1 = __webpack_require__(903); +var readdir = __webpack_require__(873); +var reader_1 = __webpack_require__(886); +var fs_sync_1 = __webpack_require__(898); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -105558,7 +104731,7 @@ exports.default = ReaderSync; /***/ }), -/* 903 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105577,8 +104750,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(891); +var fs_1 = __webpack_require__(895); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -105624,7 +104797,7 @@ exports.default = FileSystemSync; /***/ }), -/* 904 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105640,7 +104813,7 @@ exports.flatten = flatten; /***/ }), -/* 905 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105661,13 +104834,13 @@ exports.merge = merge; /***/ }), -/* 906 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(907); +const pathType = __webpack_require__(902); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -105733,13 +104906,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 907 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(908); +const pify = __webpack_require__(903); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -105782,7 +104955,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 908 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105873,7 +105046,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 909 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105881,9 +105054,9 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const fastGlob = __webpack_require__(718); -const gitIgnore = __webpack_require__(910); -const pify = __webpack_require__(911); -const slash = __webpack_require__(912); +const gitIgnore = __webpack_require__(905); +const pify = __webpack_require__(906); +const slash = __webpack_require__(907); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -105981,7 +105154,7 @@ module.exports.sync = options => { /***/ }), -/* 910 */ +/* 905 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -106450,7 +105623,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 911 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106525,7 +105698,7 @@ module.exports = (input, options) => { /***/ }), -/* 912 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106543,17 +105716,17 @@ module.exports = input => { /***/ }), -/* 913 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); -const fs = __webpack_require__(921); -const ProgressEmitter = __webpack_require__(924); +const pEvent = __webpack_require__(909); +const CpFileError = __webpack_require__(912); +const fs = __webpack_require__(916); +const ProgressEmitter = __webpack_require__(919); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -106667,12 +105840,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 914 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(915); +const pTimeout = __webpack_require__(910); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -106963,12 +106136,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 915 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(916); +const pFinally = __webpack_require__(911); class TimeoutError extends Error { constructor(message) { @@ -107014,7 +106187,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 916 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107036,12 +106209,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 917 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(918); +const NestedError = __webpack_require__(913); class CpFileError extends NestedError { constructor(message, nested) { @@ -107055,10 +106228,10 @@ module.exports = CpFileError; /***/ }), -/* 918 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(919); +var inherits = __webpack_require__(914); var NestedError = function (message, nested) { this.nested = nested; @@ -107109,7 +106282,7 @@ module.exports = NestedError; /***/ }), -/* 919 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107117,12 +106290,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(920); + module.exports = __webpack_require__(915); } /***/ }), -/* 920 */ +/* 915 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107151,16 +106324,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 921 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(922); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); +const makeDir = __webpack_require__(917); +const pEvent = __webpack_require__(909); +const CpFileError = __webpack_require__(912); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -107257,7 +106430,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 922 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107265,7 +106438,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(923); +const semver = __webpack_require__(918); const defaults = { mode: 0o777 & (~process.umask()), @@ -107414,7 +106587,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 923 */ +/* 918 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -109016,7 +108189,7 @@ function coerce (version, options) { /***/ }), -/* 924 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109057,7 +108230,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 925 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109103,12 +108276,12 @@ exports.default = module.exports; /***/ }), -/* 926 */ +/* 921 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(927); +const NestedError = __webpack_require__(922); class CpyError extends NestedError { constructor(message, nested) { @@ -109122,7 +108295,7 @@ module.exports = CpyError; /***/ }), -/* 927 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109178,7 +108351,7 @@ module.exports = NestedError; /***/ }), -/* 928 */ +/* 923 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 66f17ab579ec3..f4b91d154cbb8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -136,7 +136,7 @@ export const schema = Joi.object() browser: Joi.object() .keys({ type: Joi.string() - .valid('chrome', 'firefox', 'ie') + .valid('chrome', 'firefox', 'ie', 'msedge') .default('chrome'), logPollingMs: Joi.number().default(100), diff --git a/packages/kbn-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js index d2305d643e4d2..1f1392b02baff 100644 --- a/packages/kbn-ui-shared-deps/polyfills.js +++ b/packages/kbn-ui-shared-deps/polyfills.js @@ -20,6 +20,13 @@ require('core-js/stable'); require('regenerator-runtime/runtime'); require('custom-event-polyfill'); + +if (typeof window.Event === 'object') { + // IE11 doesn't support unknown event types, required by react-use + // https://github.com/streamich/react-use/issues/73 + window.Event = CustomEvent; +} + require('whatwg-fetch'); require('abortcontroller-polyfill/dist/polyfill-patch-fetch'); require('./vendor/childnode_remove_polyfill'); diff --git a/src/core/public/plugins/plugin_loader.test.ts b/src/core/public/plugins/plugin_loader.test.ts index e5cbffc3e2d94..b4e2c3095f14a 100644 --- a/src/core/public/plugins/plugin_loader.test.ts +++ b/src/core/public/plugins/plugin_loader.test.ts @@ -71,7 +71,7 @@ test('`loadPluginBundles` creates a script tag and loads initializer', async () // Setup a fake initializer as if a plugin bundle had actually been loaded. const fakeInitializer = jest.fn(); - coreWindow.__kbnBundles__['plugin/plugin-a'] = fakeInitializer; + coreWindow.__kbnBundles__['plugin/plugin-a'] = { plugin: fakeInitializer }; // Call the onload callback fakeScriptTag.onload(); await expect(loadPromise).resolves.toEqual(fakeInitializer); diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts index 63aba0dde2af8..bf7711055e97b 100644 --- a/src/core/public/plugins/plugin_loader.ts +++ b/src/core/public/plugins/plugin_loader.ts @@ -32,7 +32,7 @@ export type UnknownPluginInitializer = PluginInitializer new Promise>( (resolve, reject) => { - const script = document.createElement('script'); const coreWindow = (window as unknown) as CoreWindow; + const exportId = `plugin/${pluginName}`; + + const readPluginExport = () => { + const PluginExport: any = coreWindow.__kbnBundles__[exportId]; + if (typeof PluginExport?.plugin !== 'function') { + reject( + new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) + ); + } else { + resolve( + PluginExport.plugin as PluginInitializer + ); + } + }; + if (coreWindow.__kbnBundles__[exportId]) { + readPluginExport(); + return; + } + + const script = document.createElement('script'); // Assumes that all plugin bundles get put into the bundles/plugins subdirectory const bundlePath = addBasePath(`/bundles/plugin/${pluginName}/${pluginName}.plugin.js`); script.setAttribute('src', bundlePath); @@ -89,15 +108,7 @@ export const loadPluginBundle: LoadPluginBundle = < // Wire up resolve and reject script.onload = () => { cleanupTag(); - - const initializer = coreWindow.__kbnBundles__[`plugin/${pluginName}`]; - if (!initializer || typeof initializer !== 'function') { - reject( - new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) - ); - } else { - resolve(initializer as PluginInitializer); - } + readPluginExport(); }; script.onerror = () => { diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index 389d98a0818c8..da8846f6dddbb 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -26,8 +26,10 @@ import { InternalElasticsearchServiceSetup, ElasticsearchServiceSetup, ElasticsearchServiceStart, + ElasticsearchStatusMeta, } from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; +import { ServiceStatus, ServiceStatusLevels } from '../status'; const createScopedClusterClientMock = (): jest.Mocked => ({ callAsInternalUser: jest.fn(), @@ -102,6 +104,10 @@ const createInternalSetupContractMock = () => { warningNodes: [], kibanaVersion: '8.0.0', }), + status$: new BehaviorSubject>({ + level: ServiceStatusLevels.available, + summary: 'Elasticsearch is available', + }), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), }, diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index b92a6edf778ed..684f6e15caff9 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -40,6 +40,7 @@ import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; import { InternalElasticsearchServiceSetup, ElasticsearchServiceStart } from './types'; import { CallAPIOptions } from './api_types'; import { pollEsNodesVersion } from './version_check/ensure_es_version'; +import { calculateStatus$ } from './status'; /** @internal */ interface CoreClusterClients { @@ -186,6 +187,7 @@ export class ElasticsearchService adminClient: this.adminClient, dataClient, createClient: this.createClient, + status$: calculateStatus$(esNodesCompatibility$), }; } diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index cfd72a6fd5e47..2e45f710c4dcf 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -31,3 +31,4 @@ export { config, configSchema, ElasticsearchConfig } from './elasticsearch_confi export { ElasticsearchError, ElasticsearchErrorHelpers } from './errors'; export * from './api_types'; export * from './types'; +export { NodesVersionCompatibility } from './version_check/ensure_es_version'; diff --git a/src/core/server/elasticsearch/status.test.ts b/src/core/server/elasticsearch/status.test.ts new file mode 100644 index 0000000000000..dd5fb04bfd1c6 --- /dev/null +++ b/src/core/server/elasticsearch/status.test.ts @@ -0,0 +1,222 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { take } from 'rxjs/operators'; +import { Subject, of } from 'rxjs'; + +import { calculateStatus$ } from './status'; +import { ServiceStatusLevels, ServiceStatus } from '../status'; +import { ServiceStatusLevelSnapshotSerializer } from '../status/test_utils'; +import { NodesVersionCompatibility } from './version_check/ensure_es_version'; + +expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); + +const nodeInfo = { + version: '1.1.1', + ip: '1.1.1.1', + http: { + publish_address: 'https://1.1.1.1:9200', + }, + name: 'node1', +}; + +describe('calculateStatus', () => { + it('starts in unavailable', async () => { + expect( + await calculateStatus$(new Subject()) + .pipe(take(1)) + .toPromise() + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: 'Waiting for Elasticsearch', + meta: { + warningNodes: [], + incompatibleNodes: [], + }, + }); + }); + + it('changes to available when isCompatible and no warningNodes', async () => { + expect( + await calculateStatus$( + of({ isCompatible: true, kibanaVersion: '1.1.1', warningNodes: [], incompatibleNodes: [] }) + ) + .pipe(take(2)) + .toPromise() + ).toEqual({ + level: ServiceStatusLevels.available, + summary: 'Elasticsearch is available', + meta: { + warningNodes: [], + incompatibleNodes: [], + }, + }); + }); + + it('changes to degraded when isCompatible and warningNodes present', async () => { + expect( + await calculateStatus$( + of({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [nodeInfo], + incompatibleNodes: [], + // this isn't the real message, just used to test that the message + // is forwarded to the status + message: 'Some nodes are a different version', + }) + ) + .pipe(take(2)) + .toPromise() + ).toEqual({ + level: ServiceStatusLevels.degraded, + summary: 'Some nodes are a different version', + meta: { + incompatibleNodes: [], + warningNodes: [nodeInfo], + }, + }); + }); + + it('changes to critical when isCompatible is false', async () => { + expect( + await calculateStatus$( + of({ + isCompatible: false, + kibanaVersion: '2.1.1', + warningNodes: [nodeInfo], + incompatibleNodes: [nodeInfo], + // this isn't the real message, just used to test that the message + // is forwarded to the status + message: 'Incompatible with Elasticsearch', + }) + ) + .pipe(take(2)) + .toPromise() + ).toEqual({ + level: ServiceStatusLevels.critical, + summary: 'Incompatible with Elasticsearch', + meta: { + incompatibleNodes: [nodeInfo], + warningNodes: [nodeInfo], + }, + }); + }); + + it('emits status updates when node compatibility changes', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe(status => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '2.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info', + }); + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '2.1.1', + incompatibleNodes: [nodeInfo], + warningNodes: [], + message: 'Incompatible with Elasticsearch', + }); + nodeCompat$.next({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [nodeInfo], + incompatibleNodes: [], + message: 'Some nodes are incompatible', + }); + nodeCompat$.next({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [], + incompatibleNodes: [], + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [ + Object { + "http": Object { + "publish_address": "https://1.1.1.1:9200", + }, + "ip": "1.1.1.1", + "name": "node1", + "version": "1.1.1", + }, + ], + "warningNodes": Array [], + }, + "summary": "Incompatible with Elasticsearch", + }, + Object { + "level": degraded, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [ + Object { + "http": Object { + "publish_address": "https://1.1.1.1:9200", + }, + "ip": "1.1.1.1", + "name": "node1", + "version": "1.1.1", + }, + ], + }, + "summary": "Some nodes are incompatible", + }, + Object { + "level": available, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Elasticsearch is available", + }, + ] + `); + }); +}); diff --git a/src/core/server/elasticsearch/status.ts b/src/core/server/elasticsearch/status.ts new file mode 100644 index 0000000000000..1eaa338af1239 --- /dev/null +++ b/src/core/server/elasticsearch/status.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, merge, of } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { ServiceStatus, ServiceStatusLevels } from '../status'; +import { ElasticsearchStatusMeta } from './types'; +import { NodesVersionCompatibility } from './version_check/ensure_es_version'; + +export const calculateStatus$ = ( + esNodesCompatibility$: Observable +): Observable> => + merge( + of({ + level: ServiceStatusLevels.unavailable, + summary: `Waiting for Elasticsearch`, + meta: { + warningNodes: [], + incompatibleNodes: [], + }, + }), + esNodesCompatibility$.pipe( + map( + ({ + isCompatible, + message, + incompatibleNodes, + warningNodes, + }): ServiceStatus => { + if (!isCompatible) { + return { + level: ServiceStatusLevels.critical, + summary: + // Message should always be present, but this is a safe fallback + message ?? + `Some Elasticsearch nodes are not compatible with this version of Kibana`, + meta: { warningNodes, incompatibleNodes }, + }; + } else if (warningNodes.length > 0) { + return { + level: ServiceStatusLevels.degraded, + summary: + // Message should always be present, but this is a safe fallback + message ?? + `Some Elasticsearch nodes are running different versions than this version of Kibana`, + meta: { warningNodes, incompatibleNodes }, + }; + } + + return { + level: ServiceStatusLevels.available, + summary: `Elasticsearch is available`, + meta: { + warningNodes: [], + incompatibleNodes: [], + }, + }; + } + ) + ) + ); diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index ef8edecfd26ec..3d38935e9fbf0 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -22,6 +22,7 @@ import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchClientConfig } from './elasticsearch_client_config'; import { IClusterClient, ICustomClusterClient } from './cluster_client'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; +import { ServiceStatus } from '../status'; /** * @public @@ -128,4 +129,11 @@ export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceS readonly config$: Observable; }; esNodesCompatibility$: Observable; + status$: Observable>; +} + +/** @public */ +export interface ElasticsearchStatusMeta { + warningNodes: NodesVersionCompatibility['warningNodes']; + incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; } diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index 3e760ec0efabd..7bd6331978d1d 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -142,7 +142,7 @@ export const pollEsNodesVersion = ({ kibanaVersion, ignoreVersionMismatch, esVersionCheckInterval: healthCheckInterval, -}: PollEsNodesVersionOptions): Observable => { +}: PollEsNodesVersionOptions): Observable => { log.debug('Checking Elasticsearch version'); return timer(0, healthCheckInterval).pipe( exhaustMap(() => { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 56ce16a951aa2..a298f80f96d8f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -60,6 +60,7 @@ import { import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { UuidServiceSetup } from './uuid'; import { MetricsServiceSetup } from './metrics'; +import { StatusServiceSetup } from './status'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; @@ -95,6 +96,8 @@ export { ElasticsearchErrorHelpers, ElasticsearchServiceSetup, ElasticsearchServiceStart, + ElasticsearchStatusMeta, + NodesVersionCompatibility, APICaller, FakeRequest, ScopeableRequest, @@ -226,6 +229,7 @@ export { SavedObjectsUpdateResponse, SavedObjectsServiceStart, SavedObjectsServiceSetup, + SavedObjectStatusMeta, SavedObjectsDeleteOptions, ISavedObjectsRepository, SavedObjectsRepository, @@ -294,6 +298,14 @@ export { LegacyInternals, } from './legacy'; +export { + CoreStatus, + ServiceStatus, + ServiceStatusLevel, + ServiceStatusLevels, + StatusServiceSetup, +} from './status'; + /** * Plugin specific context passed to a route handler. * @@ -348,14 +360,16 @@ export interface CoreSetup; } diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 825deea99bc23..ede0d3dc9fcc7 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -31,6 +31,7 @@ import { import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; import { UuidServiceSetup } from './uuid'; import { InternalMetricsServiceSetup } from './metrics'; +import { InternalStatusServiceSetup } from './status'; /** @internal */ export interface InternalCoreSetup { @@ -38,10 +39,11 @@ export interface InternalCoreSetup { context: ContextSetup; http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; - uiSettings: InternalUiSettingsServiceSetup; + metrics: InternalMetricsServiceSetup; savedObjects: InternalSavedObjectsServiceSetup; + status: InternalStatusServiceSetup; + uiSettings: InternalUiSettingsServiceSetup; uuid: UuidServiceSetup; - metrics: InternalMetricsServiceSetup; } /** diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index c6860086e7784..0cf2ebe55ea10 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -48,6 +48,7 @@ import { findLegacyPluginSpecs } from './plugins'; import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; import { coreMock } from '../mocks'; +import { statusServiceMock } from '../status/status_service.mock'; const MockKbnServer: jest.Mock = KbnServer as any; @@ -106,6 +107,7 @@ beforeEach(() => { rendering: renderingServiceMock, metrics: metricsServiceMock.createInternalSetupContract(), uuid: uuidSetup, + status: statusServiceMock.createInternalSetupContract(), }, plugins: { 'plugin-id': 'plugin-value' }, }; diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index bb5f6d5617aae..f77230301ce02 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -306,6 +306,9 @@ export class LegacyService implements CoreService { registerType: setupDeps.core.savedObjects.registerType, getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, }, + status: { + core$: setupDeps.core.status.core$, + }, uiSettings: { register: setupDeps.core.uiSettings.register, }, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 31bf17da041af..faf73044cac4d 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -33,6 +33,7 @@ import { InternalCoreSetup, InternalCoreStart } from './internal_types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; import { metricsServiceMock } from './metrics/metrics_service.mock'; import { uuidServiceMock } from './uuid/uuid_service.mock'; +import { statusServiceMock } from './status/status_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -133,9 +134,10 @@ function createCoreSetupMock({ elasticsearch: elasticsearchServiceMock.createSetup(), http: httpMock, savedObjects: savedObjectsServiceMock.createInternalSetupContract(), + status: statusServiceMock.createSetupContract(), + metrics: metricsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), - metrics: metricsServiceMock.createSetupContract(), getStartServices: jest .fn, object, any]>, []>() .mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]), @@ -161,10 +163,11 @@ function createInternalCoreSetupMock() { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createInternalSetup(), http: httpServiceMock.createSetupContract(), - uiSettings: uiSettingsServiceMock.createSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), + status: statusServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), - metrics: metricsServiceMock.createInternalSetupContract(), + uiSettings: uiSettingsServiceMock.createSetupContract(), }; return setupDeps; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 32662f07a86f0..61d97aea97459 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -175,6 +175,9 @@ export function createPluginSetupContext( registerType: deps.savedObjects.registerType, getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit, }, + status: { + core$: deps.status.core$, + }, uiSettings: { register: deps.uiSettings.register, }, diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index b50e47b9eab73..fe4795cad11a5 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -68,7 +68,11 @@ export { SavedObjectMigrationContext, } from './migrations'; -export { SavedObjectsType, SavedObjectsTypeManagementDefinition } from './types'; +export { + SavedObjectStatusMeta, + SavedObjectsType, + SavedObjectsTypeManagementDefinition, +} from './types'; export { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects_config'; export { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 4fbadf90f4b60..466d399f653cd 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -22,4 +22,4 @@ export { IndexMigrator } from './index_migrator'; export { buildActiveMappings } from './build_active_mappings'; export { CallCluster } from './call_cluster'; export { LogFn } from './migration_logger'; -export { MigrationResult } from './migration_coordinator'; +export { MigrationResult, MigrationStatus } from './migration_coordinator'; diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts index ddd82edd93448..5ba2d0afc692e 100644 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts +++ b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts @@ -39,6 +39,8 @@ import { SavedObjectsMigrationLogger } from './migration_logger'; const DEFAULT_POLL_INTERVAL = 15000; +export type MigrationStatus = 'waiting' | 'running' | 'completed'; + export type MigrationResult = | { status: 'skipped' } | { status: 'patched' } diff --git a/src/core/server/saved_objects/migrations/index.ts b/src/core/server/saved_objects/migrations/index.ts index dc966f0797822..8ddaed3707eb0 100644 --- a/src/core/server/saved_objects/migrations/index.ts +++ b/src/core/server/saved_objects/migrations/index.ts @@ -17,6 +17,7 @@ * under the License. */ +export { MigrationResult } from './core'; export { KibanaMigrator, IKibanaMigrator } from './kibana'; export { SavedObjectMigrationFn, diff --git a/src/core/server/saved_objects/migrations/kibana/index.ts b/src/core/server/saved_objects/migrations/kibana/index.ts index 25772c4c9b0b1..df4751521ac53 100644 --- a/src/core/server/saved_objects/migrations/kibana/index.ts +++ b/src/core/server/saved_objects/migrations/kibana/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { KibanaMigrator, IKibanaMigrator } from './kibana_migrator'; +export { KibanaMigrator, IKibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts index 2ee656721abd0..257b32c1e4c23 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -17,10 +17,11 @@ * under the License. */ -import { KibanaMigrator } from './kibana_migrator'; +import { KibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; import { buildActiveMappings } from '../core'; const { mergeTypes } = jest.requireActual('./kibana_migrator'); import { SavedObjectsType } from '../../types'; +import { BehaviorSubject } from 'rxjs'; const defaultSavedObjectTypes: SavedObjectsType[] = [ { @@ -47,6 +48,20 @@ const createMigrator = ( runMigrations: jest.fn(), getActiveMappings: jest.fn(), migrateDocument: jest.fn(), + getStatus$: jest.fn( + () => + new BehaviorSubject({ + status: 'completed', + result: [ + { + status: 'migrated', + destIndex: '.test-kibana_2', + sourceIndex: '.test-kibana_1', + elapsedMs: 10, + }, + ], + }) + ), }; mockMigrator.getActiveMappings.mockReturnValue(buildActiveMappings(mergeTypes(types))); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index fd82bf282266e..336eeff99f47b 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { take } from 'rxjs/operators'; import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; import { loggingServiceMock } from '../../../logging/logging_service.mock'; @@ -79,6 +80,33 @@ describe('KibanaMigrator', () => { .filter(callClusterPath => callClusterPath === 'cat.templates'); expect(callClusterCommands.length).toBe(1); }); + + it('emits results on getMigratorResult$()', async () => { + const options = mockOptions(); + const clusterStub = jest.fn(() => ({ status: 404 })); + + options.callCluster = clusterStub; + const migrator = new KibanaMigrator(options); + const migratorStatus = migrator + .getStatus$() + .pipe(take(3)) + .toPromise(); + await migrator.runMigrations(); + const { status, result } = await migratorStatus; + expect(status).toEqual('completed'); + expect(result![0]).toMatchObject({ + destIndex: '.my-index_1', + elapsedMs: expect.any(Number), + sourceIndex: '.my-index', + status: 'migrated', + }); + expect(result![1]).toMatchObject({ + destIndex: 'other-index_1', + elapsedMs: expect.any(Number), + sourceIndex: 'other-index', + status: 'migrated', + }); + }); }); }); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index bc29061b380b8..dafd6c5341196 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -24,10 +24,17 @@ import { Logger } from 'src/core/server/logging'; import { KibanaConfigType } from 'src/core/server/kibana_config'; +import { BehaviorSubject } from 'rxjs'; import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings'; import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; import { docValidator, PropertyValidators } from '../../validation'; -import { buildActiveMappings, CallCluster, IndexMigrator } from '../core'; +import { + buildActiveMappings, + CallCluster, + IndexMigrator, + MigrationResult, + MigrationStatus, +} from '../core'; import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; import { createIndexMap } from '../core/build_index_map'; import { SavedObjectsMigrationConfigType } from '../../saved_objects_config'; @@ -46,6 +53,11 @@ export interface KibanaMigratorOptions { export type IKibanaMigrator = Pick; +export interface KibanaMigratorStatus { + status: MigrationStatus; + result?: MigrationResult[]; +} + /** * Manages the shape of mappings and documents in the Kibana index. */ @@ -58,7 +70,10 @@ export class KibanaMigrator { private readonly mappingProperties: SavedObjectsTypeMappingDefinitions; private readonly typeRegistry: ISavedObjectTypeRegistry; private readonly serializer: SavedObjectsSerializer; - private migrationResult?: Promise>; + private migrationResult?: Promise; + private readonly status$ = new BehaviorSubject({ + status: 'waiting', + }); /** * Creates an instance of KibanaMigrator. @@ -109,12 +124,20 @@ export class KibanaMigrator { Array<{ status: string }> > { if (this.migrationResult === undefined || rerun) { - this.migrationResult = this.runMigrationsInternal(); + this.status$.next({ status: 'running' }); + this.migrationResult = this.runMigrationsInternal().then(result => { + this.status$.next({ status: 'completed', result }); + return result; + }); } return this.migrationResult; } + public getStatus$() { + return this.status$.asObservable(); + } + private runMigrationsInternal() { const kibanaIndexName = this.kibanaConfig.index; const indexMap = createIndexMap({ diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 9fe32b14e6450..7ba4613c857d7 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -17,6 +17,8 @@ * under the License. */ +import { BehaviorSubject } from 'rxjs'; + import { SavedObjectsService, InternalSavedObjectsServiceSetup, @@ -29,6 +31,7 @@ import { savedObjectsClientProviderMock } from './service/lib/scoped_client_prov import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; +import { ServiceStatusLevels } from '../status'; type SavedObjectsServiceContract = PublicMethodsOf; @@ -75,6 +78,10 @@ const createSetupContractMock = () => { const createInternalSetupContractMock = () => { const internalSetupContract: jest.Mocked = { ...createSetupContractMock(), + status$: new BehaviorSubject({ + level: ServiceStatusLevels.available, + summary: `SavedObjects is available`, + }), }; return internalSetupContract; }; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index aa440c6454569..62027928c0bb5 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -17,8 +17,8 @@ * under the License. */ -import { Subject } from 'rxjs'; -import { first, filter, take } from 'rxjs/operators'; +import { Subject, Observable } from 'rxjs'; +import { first, filter, take, switchMap } from 'rxjs/operators'; import { CoreService } from '../../types'; import { SavedObjectsClient, @@ -38,7 +38,7 @@ import { SavedObjectConfig, } from './saved_objects_config'; import { KibanaRequest, InternalHttpServiceSetup } from '../http'; -import { SavedObjectsClientContract, SavedObjectsType } from './types'; +import { SavedObjectsClientContract, SavedObjectsType, SavedObjectStatusMeta } from './types'; import { ISavedObjectsRepository, SavedObjectsRepository } from './service/lib/repository'; import { SavedObjectsClientFactoryProvider, @@ -50,6 +50,8 @@ import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objec import { PropertyValidators } from './validation'; import { SavedObjectsSerializer } from './serialization'; import { registerRoutes } from './routes'; +import { ServiceStatus } from '../status'; +import { calculateStatus$ } from './status'; /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to @@ -164,7 +166,9 @@ export interface SavedObjectsServiceSetup { /** * @internal */ -export type InternalSavedObjectsServiceSetup = SavedObjectsServiceSetup; +export interface InternalSavedObjectsServiceSetup extends SavedObjectsServiceSetup { + status$: Observable>; +} /** * Saved Objects is Kibana's data persisentence mechanism allowing plugins to @@ -321,6 +325,10 @@ export class SavedObjectsService }); return { + status$: calculateStatus$( + this.migrator$.pipe(switchMap(migrator => migrator.getStatus$())), + setupDeps.elasticsearch.status$ + ), setClientFactoryProvider: provider => { if (this.started) { throw new Error('cannot call `setClientFactoryProvider` after service startup.'); diff --git a/src/core/server/saved_objects/status.test.ts b/src/core/server/saved_objects/status.test.ts new file mode 100644 index 0000000000000..8efea1e2c00c6 --- /dev/null +++ b/src/core/server/saved_objects/status.test.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { of, Observable } from 'rxjs'; +import { ServiceStatus, ServiceStatusLevels } from '../status'; +import { calculateStatus$ } from './status'; +import { take } from 'rxjs/operators'; + +describe('calculateStatus$', () => { + const expectUnavailableDueToEs = (status$: Observable) => + expect(status$.pipe(take(1)).toPromise()).resolves.toEqual({ + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is not available without a healthy Elasticearch connection`, + }); + + const expectUnavailableDueToMigrations = (status$: Observable) => + expect(status$.pipe(take(1)).toPromise()).resolves.toEqual({ + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is waiting to start migrations`, + }); + + describe('when elasticsearch is unavailable', () => { + const esStatus$ = of({ + level: ServiceStatusLevels.unavailable, + summary: 'xxx', + }); + + it('is unavailable before migrations have ran', async () => { + await expectUnavailableDueToEs(calculateStatus$(of(), esStatus$)); + }); + it('is unavailable after migrations have ran', async () => { + await expectUnavailableDueToEs( + calculateStatus$(of({ status: 'completed', result: [] }), esStatus$) + ); + }); + }); + + describe('when elasticsearch is critical', () => { + const esStatus$ = of({ + level: ServiceStatusLevels.critical, + summary: 'xxx', + }); + + it('is unavailable before migrations have ran', async () => { + await expectUnavailableDueToEs(calculateStatus$(of(), esStatus$)); + }); + it('is unavailable after migrations have ran', async () => { + await expectUnavailableDueToEs( + calculateStatus$( + of({ status: 'completed', result: [{ status: 'migrated' } as any] }), + esStatus$ + ) + ); + }); + }); + + describe('when elasticsearch is available', () => { + const esStatus$ = of({ + level: ServiceStatusLevels.available, + summary: 'Available', + }); + + it('is unavailable before migrations have ran', async () => { + await expectUnavailableDueToMigrations(calculateStatus$(of(), esStatus$)); + }); + it('is unavailable while migrations are running', async () => { + await expect( + calculateStatus$(of({ status: 'running' }), esStatus$) + .pipe(take(2)) + .toPromise() + ).resolves.toEqual({ + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is running migrations`, + }); + }); + it('is available after migrations have ran', async () => { + await expect( + calculateStatus$( + of({ status: 'completed', result: [{ status: 'skipped' }, { status: 'patched' }] }), + esStatus$ + ) + .pipe(take(2)) + .toPromise() + ).resolves.toEqual({ + level: ServiceStatusLevels.available, + summary: `SavedObjects service has completed migrations and is available`, + meta: { + migratedIndices: { + migrated: 0, + patched: 1, + skipped: 1, + }, + }, + }); + }); + }); + + describe('when elasticsearch is degraded', () => { + const esStatus$ = of({ level: ServiceStatusLevels.degraded, summary: 'xxx' }); + + it('is unavailable before migrations have ran', async () => { + await expectUnavailableDueToMigrations(calculateStatus$(of(), esStatus$)); + }); + it('is degraded after migrations have ran', async () => { + await expect( + calculateStatus$( + of([{ status: 'skipped' }]), + esStatus$ + ) + .pipe(take(2)) + .toPromise() + ).resolves.toEqual({ + level: ServiceStatusLevels.degraded, + summary: 'SavedObjects service is degraded due to Elasticsearch: [xxx]', + }); + }); + }); +}); diff --git a/src/core/server/saved_objects/status.ts b/src/core/server/saved_objects/status.ts new file mode 100644 index 0000000000000..66a6e2baa17a7 --- /dev/null +++ b/src/core/server/saved_objects/status.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, combineLatest } from 'rxjs'; +import { startWith, map } from 'rxjs/operators'; +import { ServiceStatus, ServiceStatusLevels } from '../status'; +import { SavedObjectStatusMeta } from './types'; +import { KibanaMigratorStatus } from './migrations/kibana'; + +export const calculateStatus$ = ( + rawMigratorStatus$: Observable, + elasticsearchStatus$: Observable +): Observable> => { + const migratorStatus$: Observable> = rawMigratorStatus$.pipe( + map(migrationStatus => { + if (migrationStatus.status === 'waiting') { + return { + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is waiting to start migrations`, + }; + } else if (migrationStatus.status === 'running') { + return { + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is running migrations`, + }; + } + + const statusCounts: SavedObjectStatusMeta['migratedIndices'] = { migrated: 0, skipped: 0 }; + if (migrationStatus.result) { + migrationStatus.result.forEach(({ status }) => { + statusCounts[status] = (statusCounts[status] ?? 0) + 1; + }); + } + + return { + level: ServiceStatusLevels.available, + summary: `SavedObjects service has completed migrations and is available`, + meta: { + migratedIndices: statusCounts, + }, + }; + }), + startWith({ + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is waiting to start migrations`, + }) + ); + + return combineLatest([elasticsearchStatus$, migratorStatus$]).pipe( + map(([esStatus, migratorStatus]) => { + if (esStatus.level >= ServiceStatusLevels.unavailable) { + return { + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is not available without a healthy Elasticearch connection`, + }; + } else if (migratorStatus.level === ServiceStatusLevels.unavailable) { + return migratorStatus; + } else if (esStatus.level === ServiceStatusLevels.degraded) { + return { + level: esStatus.level, + summary: `SavedObjects service is degraded due to Elasticsearch: [${esStatus.summary}]`, + }; + } else { + return migratorStatus; + } + }) + ); +}; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 962965a08f8b2..f14e9d9efb5e3 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -46,6 +46,19 @@ export { SavedObjectsMigrationVersion, } from '../../types'; +/** + * Meta information about the SavedObjectService's status. Available to plugins via {@link CoreSetup.status}. + * + * @public + */ +export interface SavedObjectStatusMeta { + migratedIndices: { + [status: string]: number; + skipped: number; + migrated: number; + }; +} + /** * * @public diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e4e2b8d7adbb7..f3e3b7736d8d3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -638,6 +638,8 @@ export interface CoreSetup ISavedObjectTypeRegistry; } +// @public +export interface SavedObjectStatusMeta { + // (undocumented) + migratedIndices: { + [status: string]: number; + skipped: number; + migrated: number; + }; +} + // @public (undocumented) export interface SavedObjectsType { convertToAliasScript?: string; @@ -2237,6 +2283,38 @@ export class ScopedClusterClient implements IScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; } +// @public +export interface ServiceStatus | unknown = unknown> { + detail?: string; + documentationUrl?: string; + level: ServiceStatusLevel; + meta?: Meta; + summary: string; +} + +// @public +export type ServiceStatusLevel = typeof ServiceStatusLevels[keyof typeof ServiceStatusLevels]; + +// @public +export const ServiceStatusLevels: Readonly<{ + available: Readonly<{ + toString: () => "available"; + valueOf: () => 0; + }>; + degraded: Readonly<{ + toString: () => "degraded"; + valueOf: () => 1; + }>; + unavailable: Readonly<{ + toString: () => "unavailable"; + valueOf: () => 2; + }>; + critical: Readonly<{ + toString: () => "critical"; + valueOf: () => 3; + }>; +}>; + // @public export interface SessionCookieValidationResult { isValid: boolean; @@ -2274,6 +2352,11 @@ export type SharedGlobalConfig = RecursiveReadonly_2<{ // @public export type StartServicesAccessor = () => Promise<[CoreStart, TPluginsStart, TStart]>; +// @public +export interface StatusServiceSetup { + core$: Observable; +} + // @public export type StringValidation = StringValidationRegex | StringValidationRegexString; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 53d1b742a6494..5d535c9845724 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -85,3 +85,9 @@ export const mockMetricsService = metricsServiceMock.create(); jest.doMock('./metrics/metrics_service', () => ({ MetricsService: jest.fn(() => mockMetricsService), })); + +import { statusServiceMock } from './status/status_service.mock'; +export const mockStatusService = statusServiceMock.create(); +jest.doMock('./status/status_service', () => ({ + StatusService: jest.fn(() => mockStatusService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index a4b5a9d81df20..24c41d511180a 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -29,6 +29,7 @@ import { mockUiSettingsService, mockRenderingService, mockMetricsService, + mockStatusService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -63,6 +64,7 @@ test('sets up services on "setup"', async () => { expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); expect(mockMetricsService.setup).not.toHaveBeenCalled(); + expect(mockStatusService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -74,6 +76,7 @@ test('sets up services on "setup"', async () => { expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1); expect(mockRenderingService.setup).toHaveBeenCalledTimes(1); expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); + expect(mockStatusService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -141,6 +144,7 @@ test('stops services on "stop"', async () => { expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.stop).not.toHaveBeenCalled(); expect(mockMetricsService.stop).not.toHaveBeenCalled(); + expect(mockStatusService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -151,6 +155,7 @@ test('stops services on "stop"', async () => { expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.stop).toHaveBeenCalledTimes(1); expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); + expect(mockStatusService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { @@ -167,6 +172,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); expect(mockMetricsService.setup).not.toHaveBeenCalled(); + expect(mockStatusService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -187,4 +193,5 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockMetricsService.setup).not.toHaveBeenCalled(); + expect(mockStatusService.setup).not.toHaveBeenCalled(); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 222be572b75e4..07ea431dd3a0d 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -36,6 +36,9 @@ import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; import { MetricsService, opsConfig } from './metrics'; +import { CapabilitiesService } from './capabilities'; +import { UuidService } from './uuid'; +import { StatusService } from './status/status_service'; import { config as cspConfig } from './csp'; import { config as elasticsearchConfig } from './elasticsearch'; @@ -50,8 +53,6 @@ import { mapToObject } from '../utils'; import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart } from './internal_types'; -import { CapabilitiesService } from './capabilities'; -import { UuidService } from './uuid'; const coreId = Symbol('core'); const rootConfigPath = ''; @@ -70,6 +71,7 @@ export class Server { private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; private readonly metrics: MetricsService; + private readonly status: StatusService; private readonly coreApp: CoreApp; private pluginsInitialized?: boolean; @@ -95,6 +97,7 @@ export class Server { this.capabilities = new CapabilitiesService(core); this.uuid = new UuidService(core); this.metrics = new MetricsService(core); + this.status = new StatusService(core); this.coreApp = new CoreApp(core); } @@ -145,15 +148,21 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const statusSetup = this.status.setup({ + elasticsearch: elasticsearchServiceSetup, + savedObjects: savedObjectsSetup, + }); + const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, - uiSettings: uiSettingsSetup, + metrics: metricsSetup, savedObjects: savedObjectsSetup, + status: statusSetup, + uiSettings: uiSettingsSetup, uuid: uuidSetup, - metrics: metricsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -220,6 +229,7 @@ export class Server { await this.uiSettings.stop(); await this.rendering.stop(); await this.metrics.stop(); + await this.status.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup, rendering: RenderingServiceSetup) { diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts new file mode 100644 index 0000000000000..7516e82ee784d --- /dev/null +++ b/src/core/server/status/get_summary_status.test.ts @@ -0,0 +1,180 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ServiceStatus, ServiceStatusLevels } from './types'; +import { getSummaryStatus } from './get_summary_status'; + +describe('getSummaryStatus', () => { + const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available' }; + const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'This is degraded!', + }; + const unavailable: ServiceStatus = { + level: ServiceStatusLevels.unavailable, + summary: 'This is unavailable!', + }; + const critical: ServiceStatus = { + level: ServiceStatusLevels.critical, + summary: 'This is critical!', + }; + + it('returns available when all status are available', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: available, + s2: available, + s3: available, + }) + ) + ).toMatchObject({ + level: ServiceStatusLevels.available, + }); + }); + + it('returns degraded when the worst status is degraded', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: available, + s2: degraded, + s3: available, + }) + ) + ).toMatchObject({ + level: ServiceStatusLevels.degraded, + }); + }); + + it('returns unavailable when the worst status is unavailable', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: available, + s2: degraded, + s3: unavailable, + }) + ) + ).toMatchObject({ + level: ServiceStatusLevels.unavailable, + }); + }); + + it('returns critical when the worst status is critical', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: critical, + s2: degraded, + s3: unavailable, + }) + ) + ).toMatchObject({ + level: ServiceStatusLevels.critical, + }); + }); + + describe('summary', () => { + describe('when a single service is at highest level', () => { + it('returns all information about that single service', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[s2]: Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }); + }); + }); + + describe('when multiple services is at highest level', () => { + it('returns aggregated information about the affected services', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + s3: { + level: ServiceStatusLevels.unavailable, + summary: 'Proin mattis', + detail: 'Nunc quis nulla at mi lobortis pretium.', + documentationUrl: 'http://helpmenow.com/problem2', + meta: { + other: { data: 'over there' }, + }, + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[2] services are unavailable', + detail: 'See the status page for more information', + meta: { + affectedServices: { + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + s3: { + level: ServiceStatusLevels.unavailable, + summary: 'Proin mattis', + detail: 'Nunc quis nulla at mi lobortis pretium.', + documentationUrl: 'http://helpmenow.com/problem2', + meta: { + other: { data: 'over there' }, + }, + }, + }, + }, + }); + }); + }); + }); +}); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts new file mode 100644 index 0000000000000..748a54f0bf8bb --- /dev/null +++ b/src/core/server/status/get_summary_status.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types'; + +/** + * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. + * @param statuses + */ +export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => { + const grouped = groupByLevel(statuses); + const highestSeverityLevel = getHighestSeverityLevel(grouped.keys()); + const highestSeverityGroup = grouped.get(highestSeverityLevel)!; + + if (highestSeverityLevel === ServiceStatusLevels.available) { + return { + level: ServiceStatusLevels.available, + summary: `All services are available`, + }; + } else if (highestSeverityGroup.size === 1) { + const [serviceName, status] = [...highestSeverityGroup.entries()][0]; + return { + ...status, + summary: `[${serviceName}]: ${status.summary!}`, + }; + } else { + return { + level: highestSeverityLevel, + summary: `[${highestSeverityGroup.size}] services are ${highestSeverityLevel.toString()}`, + // TODO: include URL to status page + detail: `See the status page for more information`, + meta: { + affectedServices: Object.fromEntries([...highestSeverityGroup]), + }, + }; + } +}; + +const groupByLevel = ( + statuses: Array<[string, ServiceStatus]> +): Map> => { + const byLevel = new Map>(); + + for (const [serviceName, status] of statuses) { + let levelMap = byLevel.get(status.level); + if (!levelMap) { + levelMap = new Map(); + byLevel.set(status.level, levelMap); + } + + levelMap.set(serviceName, status); + } + + return byLevel; +}; + +const getHighestSeverityLevel = (levels: Iterable): ServiceStatusLevel => { + const sorted = [...levels].sort((a, b) => { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else { + return 0; + } + }); + return sorted[sorted.length - 1] ?? ServiceStatusLevels.available; +}; diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js b/src/core/server/status/index.ts similarity index 71% rename from src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js rename to src/core/server/status/index.ts index 9c3e1c8180eb5..c39115d55a682 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js +++ b/src/core/server/status/index.ts @@ -17,14 +17,5 @@ * under the License. */ -describe('Point Series Agg Response', function() { - require('./_main'); - require('./_add_to_siri'); - require('./_fake_x_aspect'); - require('./_get_aspects'); - require('./_get_point'); - require('./_get_series'); - require('./_init_x_axis'); - require('./_init_y_axis'); - require('./_ordered_date_axis'); -}); +export { StatusService } from './status_service'; +export * from './types'; diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts new file mode 100644 index 0000000000000..d550c2f06750b --- /dev/null +++ b/src/core/server/status/status_service.mock.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { StatusService } from './status_service'; +import { + InternalStatusServiceSetup, + StatusServiceSetup, + ServiceStatusLevels, + ServiceStatus, + CoreStatus, +} from './types'; +import { BehaviorSubject } from 'rxjs'; + +const available: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'Service is working', +}; +const availableCoreStatus: CoreStatus = { + elasticsearch: available, + savedObjects: available, +}; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + core$: new BehaviorSubject(availableCoreStatus), + }; + + return setupContract; +}; + +const createInternalSetupContractMock = () => { + const setupContract: jest.Mocked = { + core$: new BehaviorSubject(availableCoreStatus), + overall$: new BehaviorSubject(available), + }; + + return setupContract; +}; + +type StatusServiceContract = PublicMethodsOf; + +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), + start: jest.fn(), + stop: jest.fn(), + }; + return mocked; +}; + +export const statusServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, + createInternalSetupContract: createInternalSetupContractMock, +}; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts new file mode 100644 index 0000000000000..6d92a266369b9 --- /dev/null +++ b/src/core/server/status/status_service.test.ts @@ -0,0 +1,229 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { of, BehaviorSubject } from 'rxjs'; + +import { ServiceStatus, ServiceStatusLevels, CoreStatus } from './types'; +import { StatusService } from './status_service'; +import { first } from 'rxjs/operators'; +import { mockCoreContext } from '../core_context.mock'; +import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; + +expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); + +describe('StatusService', () => { + const available: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'Available', + }; + const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'This is degraded!', + }; + + describe('setup', () => { + describe('core$', () => { + it('rolls up core status observables into single observable', async () => { + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(degraded), + }, + }); + expect(await setup.core$.pipe(first()).toPromise()).toEqual({ + elasticsearch: available, + savedObjects: degraded, + }); + }); + + it('replays last event', async () => { + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(degraded), + }, + }); + const subResult1 = await setup.core$.pipe(first()).toPromise(); + const subResult2 = await setup.core$.pipe(first()).toPromise(); + const subResult3 = await setup.core$.pipe(first()).toPromise(); + expect(subResult1).toEqual({ + elasticsearch: available, + savedObjects: degraded, + }); + expect(subResult2).toEqual({ + elasticsearch: available, + savedObjects: degraded, + }); + expect(subResult3).toEqual({ + elasticsearch: available, + savedObjects: degraded, + }); + }); + + it('does not emit duplicate events', () => { + const elasticsearch$ = new BehaviorSubject(available); + const savedObjects$ = new BehaviorSubject(degraded); + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: elasticsearch$, + }, + savedObjects: { + status$: savedObjects$, + }, + }); + + const statusUpdates: CoreStatus[] = []; + const subscription = setup.core$.subscribe(status => statusUpdates.push(status)); + + elasticsearch$.next(available); + elasticsearch$.next(available); + elasticsearch$.next({ + level: ServiceStatusLevels.available, + summary: `Wow another summary`, + }); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(available); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "elasticsearch": Object { + "level": available, + "summary": "Available", + }, + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + Object { + "elasticsearch": Object { + "level": available, + "summary": "Wow another summary", + }, + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + Object { + "elasticsearch": Object { + "level": available, + "summary": "Wow another summary", + }, + "savedObjects": Object { + "level": available, + "summary": "Available", + }, + }, + ] + `); + }); + }); + + describe('overall$', () => { + it('exposes an overall summary', async () => { + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: of(degraded), + }, + savedObjects: { + status$: of(degraded), + }, + }); + expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + }); + }); + + it('replays last event', async () => { + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: of(degraded), + }, + savedObjects: { + status$: of(degraded), + }, + }); + const subResult1 = await setup.overall$.pipe(first()).toPromise(); + const subResult2 = await setup.overall$.pipe(first()).toPromise(); + const subResult3 = await setup.overall$.pipe(first()).toPromise(); + expect(subResult1).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + }); + expect(subResult2).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + }); + expect(subResult3).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + }); + }); + + it('does not emit duplicate events', () => { + const elasticsearch$ = new BehaviorSubject(available); + const savedObjects$ = new BehaviorSubject(degraded); + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: elasticsearch$, + }, + savedObjects: { + status$: savedObjects$, + }, + }); + + const statusUpdates: ServiceStatus[] = []; + const subscription = setup.overall$.subscribe(status => statusUpdates.push(status)); + + elasticsearch$.next(available); + elasticsearch$.next(available); + elasticsearch$.next({ + level: ServiceStatusLevels.available, + summary: `Wow another summary`, + }); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(available); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": degraded, + "summary": "[savedObjects]: This is degraded!", + }, + Object { + "level": available, + "summary": "All services are available", + }, + ] + `); + }); + }); + }); +}); diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts new file mode 100644 index 0000000000000..b6697d8221951 --- /dev/null +++ b/src/core/server/status/status_service.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable max-classes-per-file */ + +import { Observable, combineLatest } from 'rxjs'; +import { map, distinctUntilChanged, shareReplay } from 'rxjs/operators'; +import { isDeepStrictEqual } from 'util'; + +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; +import { InternalElasticsearchServiceSetup } from '../elasticsearch'; +import { InternalSavedObjectsServiceSetup } from '../saved_objects'; + +import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; +import { getSummaryStatus } from './get_summary_status'; + +interface SetupDeps { + elasticsearch: Pick; + savedObjects: Pick; +} + +export class StatusService implements CoreService { + private readonly logger: Logger; + + constructor(coreContext: CoreContext) { + this.logger = coreContext.logger.get('status'); + } + + public setup(core: SetupDeps) { + const core$ = this.setupCoreStatus(core); + const overall$: Observable = core$.pipe( + map(coreStatus => { + const summary = getSummaryStatus(Object.entries(coreStatus)); + this.logger.debug(`Recalculated overall status`, { status: summary }); + return summary; + }), + distinctUntilChanged(isDeepStrictEqual) + ); + + return { + core$, + overall$, + }; + } + + public start() {} + + public stop() {} + + private setupCoreStatus({ elasticsearch, savedObjects }: SetupDeps): Observable { + return combineLatest([elasticsearch.status$, savedObjects.status$]).pipe( + map(([elasticsearchStatus, savedObjectsStatus]) => ({ + elasticsearch: elasticsearchStatus, + savedObjects: savedObjectsStatus, + })), + distinctUntilChanged(isDeepStrictEqual), + shareReplay(1) + ); + } +} diff --git a/src/core/server/status/test_utils.ts b/src/core/server/status/test_utils.ts new file mode 100644 index 0000000000000..765fa8771f375 --- /dev/null +++ b/src/core/server/status/test_utils.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ServiceStatusLevels, ServiceStatusLevel } from './types'; + +export const ServiceStatusLevelSnapshotSerializer: jest.SnapshotSerializerPlugin = { + test: (val: any) => Object.values(ServiceStatusLevels).includes(val), + print: (val: ServiceStatusLevel) => val.toString(), +}; diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts new file mode 100644 index 0000000000000..84a7356c66bbf --- /dev/null +++ b/src/core/server/status/types.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { deepFreeze } from '../../utils'; + +/** + * The current status of a service at a point in time. + * + * @typeParam Meta - JSON-serializable object. Plugins should export this type to allow other plugins to read the `meta` + * field in a type-safe way. + * @public + */ +export interface ServiceStatus | unknown = unknown> { + /** + * The current availability level of the service. + */ + level: ServiceStatusLevel; + /** + * A high-level summary of the service status. + */ + summary: string; + /** + * A more detailed description of the service status. + */ + detail?: string; + /** + * A URL to open in a new tab about how to resolve or troubleshoot the problem. + */ + documentationUrl?: string; + /** + * Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, + * machine-readable information about the service status. May include status information for underlying features. + */ + meta?: Meta; +} + +/** + * The current "level" of availability of a service. + * + * @remarks + * The values implement `valueOf` to allow for easy comparisons between status levels with <, >, etc. Higher values + * represent higher severities. Note that the default `Array.prototype.sort` implementation does not correctly sort + * these values. + * + * A snapshot serializer is available in `src/core/server/test_utils` to ease testing of these values with Jest. + * + * @public + */ +export const ServiceStatusLevels = deepFreeze({ + /** + * Everything is working! + */ + available: { + toString: () => 'available', + valueOf: () => 0, + }, + /** + * Some features may not be working. + */ + degraded: { + toString: () => 'degraded', + valueOf: () => 1, + }, + /** + * The service is unavailable, but other functions that do not depend on this service should work. + */ + unavailable: { + toString: () => 'unavailable', + valueOf: () => 2, + }, + /** + * Block all user functions and display the status page, reserved for Core services only. + */ + critical: { + toString: () => 'critical', + valueOf: () => 3, + }, +}); + +/** + * A convenience type that represents the union of each value in {@link ServiceStatusLevels}. + * @public + */ +export type ServiceStatusLevel = typeof ServiceStatusLevels[keyof typeof ServiceStatusLevels]; + +/** + * Status of core services. + * + * @internalRemarks + * Only contains entries for backend services that could have a non-available `status`. + * For example, `context` cannot possibly be broken, so it is not included. + * + * @public + */ +export interface CoreStatus { + elasticsearch: ServiceStatus; + savedObjects: ServiceStatus; +} + +/** + * API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. + * @public + */ +export interface StatusServiceSetup { + /** + * Current status for all Core services. + */ + core$: Observable; +} + +/** @internal */ +export interface InternalStatusServiceSetup extends StatusServiceSetup { + /** + * Overall system status used for HTTP API + */ + overall$: Observable; +} diff --git a/src/core/server/test_utils.ts b/src/core/server/test_utils.ts index 470b1c2d135b7..f7e6fbcd0c131 100644 --- a/src/core/server/test_utils.ts +++ b/src/core/server/test_utils.ts @@ -18,3 +18,4 @@ */ export { createHttpServer } from './http/test_utils'; +export { ServiceStatusLevelSnapshotSerializer } from './status/test_utils'; diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index 8bb3fb20cea8b..ba3cd1280f34b 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -163,6 +163,7 @@ interface Options { accept: boolean; docs: boolean; help: boolean; + filter: string; } async function run( @@ -205,6 +206,7 @@ async function run( const extraFlags: string[] = []; const opts = (getopts(process.argv.slice(2), { boolean: ['accept', 'docs', 'help'], + string: ['filter'], default: { project: undefined, }, @@ -222,6 +224,8 @@ async function run( opts.help = true; } + const folders = ['core/public', 'core/server', 'plugins/data/server', 'plugins/data/public']; + if (opts.help) { process.stdout.write( dedent(chalk` @@ -240,9 +244,13 @@ async function run( {dim # Checks for and automatically accepts and updates documentation for any changes to the Kibana Core API} {dim $} node scripts/check_published_api_changes --accept + {dim # Only checks the core/public directory} + {dim $} node scripts/check_published_api_changes --filter=core/public + Options: --accept {dim Accepts all changes by updating the API Review files and documentation} --docs {dim Updates the Core API documentation} + --only {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} --help {dim Show this message} `) ); @@ -258,9 +266,11 @@ async function run( return false; } - const folders = ['core/public', 'core/server', 'plugins/data/server', 'plugins/data/public']; - - const results = await Promise.all(folders.map(folder => run(folder, { log, opts }))); + const results = await Promise.all( + folders + .filter(folder => (opts.filter.length ? folder.match(opts.filter) : true)) + .map(folder => run(folder, { log, opts })) + ); if (results.find(r => r === false) !== undefined) { process.exitCode = 1; diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts index 023e6ebb7125c..badea68eec19f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts @@ -23,7 +23,7 @@ import { createInputControlVisController } from './vis_controller'; import { getControlsTab } from './components/editor/controls_tab'; import { OptionsTab } from './components/editor/options_tab'; import { InputControlVisDependencies } from './plugin'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) { const InputControlVisController = createInputControlVisController(deps); diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index 180ff13cdddc0..a3a99a0ded523 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -72,6 +72,7 @@ export async function buildServices( const services = { savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, + search: plugins.data.search, chrome: core.chrome, overlays: core.overlays, }; diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index e6421142f6666..0a81ca0222b0a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -17,6 +17,8 @@ * under the License. */ import { DiscoverServices } from './build_services'; +import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; +import { search } from '../../../../../plugins/data/public'; let angularModule: any = null; let services: DiscoverServices | null = null; @@ -50,8 +52,6 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ setTrackedUrl: (url: string) => void; }>('urlTracker'); -import { search } from '../../../../../plugins/data/public'; -import { createGetterSetter } from '../../../../../plugins/kibana_utils/common'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; export { unhashUrl, @@ -76,5 +76,3 @@ export { EsQuerySortValue, SortDirection, } from '../../../../../plugins/data/public'; -// @ts-ignore -export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx index f788347ac016c..8c55622e4c604 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx @@ -46,9 +46,10 @@ import { IUiSettingsClient } from 'kibana/public'; import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; import { Subscription } from 'rxjs'; import { getServices } from '../../../kibana_services'; +import { Chart as IChart } from '../helpers/point_series'; export interface DiscoverHistogramProps { - chartData: any; + chartData: IChart; timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; } @@ -163,7 +164,7 @@ export class DiscoverHistogram extends Component { - const xAxisFormat = this.props.chartData.xAxisFormat.params.pattern; + const xAxisFormat = this.props.chartData.xAxisFormat.params!.pattern; return moment(val).format(xAxisFormat); }; @@ -208,18 +209,19 @@ export class DiscoverHistogram extends Component domainStart ? domainStart : data[0].x; + const domainMin = data[0]?.x > domainStart ? domainStart : data[0]?.x; const domainMax = domainEnd - xInterval > lastXValue ? domainEnd - xInterval : lastXValue; const xDomain = { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index fb38f3e7d4c49..d068e824a3e0a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -11,7 +11,7 @@

{{screenTitle}}

query="state.query" saved-query-id="state.savedQuery" screen-title="screenTitle" - show-date-picker="enableTimeRangeSelector" + show-date-picker="indexPattern.isTimeBased()" show-save-query="showSaveQuery" show-search-bar="true" use-default-behaviors="true" diff --git a/src/legacy/ui/public/agg_response/point_series/index.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/index.ts similarity index 100% rename from src/legacy/ui/public/agg_response/point_series/index.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/index.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/point_series.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/point_series.ts new file mode 100644 index 0000000000000..02dd024b09812 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/point_series.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { uniq } from 'lodash'; +import { Duration, Moment } from 'moment'; +import { Unit } from '@elastic/datemath'; + +import { SerializedFieldFormat } from '../../../../../../../../plugins/expressions/common/types'; + +export interface Column { + id: string; + name: string; +} + +export interface Row { + [key: string]: number | 'NaN'; +} + +export interface Table { + columns: Column[]; + rows: Row[]; +} + +interface HistogramParams { + date: true; + interval: Duration; + intervalESValue: number; + intervalESUnit: Unit; + format: string; + bounds: { + min: Moment; + max: Moment; + }; +} +export interface Dimension { + accessor: 0 | 1; + format: SerializedFieldFormat<{ pattern: string }>; +} + +export interface Dimensions { + x: Dimension & { params: HistogramParams }; + y: Dimension; +} + +interface Ordered { + date: true; + interval: Duration; + intervalESUnit: string; + intervalESValue: number; + min: Moment; + max: Moment; +} +export interface Chart { + values: Array<{ + x: number; + y: number; + }>; + xAxisOrderedValues: number[]; + xAxisFormat: Dimension['format']; + xAxisLabel: Column['name']; + yAxisLabel?: Column['name']; + ordered: Ordered; +} + +export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { + const { x, y } = dimensions; + const xAccessor = table.columns[x.accessor].id; + const yAccessor = table.columns[y.accessor].id; + const chart = {} as Chart; + + chart.xAxisOrderedValues = uniq(table.rows.map(r => r[xAccessor] as number)); + chart.xAxisFormat = x.format; + chart.xAxisLabel = table.columns[x.accessor].name; + + const { intervalESUnit, intervalESValue, interval, bounds } = x.params; + chart.ordered = { + date: true, + interval, + intervalESUnit, + intervalESValue, + min: bounds.min, + max: bounds.max, + }; + + chart.yAxisLabel = table.columns[y.accessor].name; + + chart.values = table.rows + .filter(row => row && row[yAccessor] !== 'NaN') + .map(row => ({ + x: row[xAccessor] as number, + y: row[yAccessor] as number, + })); + + return chart; +}; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/response_handler.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/response_handler.js index 0c19c10841535..04ccb67ec7e25 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/response_handler.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/response_handler.js @@ -17,7 +17,8 @@ * under the License. */ -import { buildPointSeriesData, getServices } from '../../kibana_services'; +import { getServices } from '../../kibana_services'; +import { buildPointSeriesData } from './helpers'; function tableResponseHandler(table, dimensions) { const converted = { tables: [] }; diff --git a/src/legacy/core_plugins/management/public/index.ts b/src/legacy/core_plugins/kibana/public/index.ts similarity index 70% rename from src/legacy/core_plugins/management/public/index.ts rename to src/legacy/core_plugins/kibana/public/index.ts index bc3737524e125..a4fffc6eec26d 100644 --- a/src/legacy/core_plugins/management/public/index.ts +++ b/src/legacy/core_plugins/kibana/public/index.ts @@ -17,22 +17,7 @@ * under the License. */ -/** - * Static np-ready code, re-exported here so consumers can import from - * `src/legacy/core_plugins/management/public` - * - * @public - */ - export { - ManagementSetup, - ManagementStart, - plugin, - IndexPatternCreationConfig, - IndexPatternListConfig, -} from './np_ready'; - -export { - processImportResponse, ProcessedImportResponse, -} from '../../kibana/public/management/sections/objects/lib/process_import_response'; + processImportResponse, +} from './management/sections/objects/lib/process_import_response'; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index bceb3fa7eef8a..0a026a5e0c310 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -46,7 +46,6 @@ import './discover/legacy'; import './visualize/legacy'; import './management'; import './dev_tools'; -import 'ui/agg_response'; import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/public'; import 'leaflet'; import { localApplicationService } from './local_application_service'; diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 7261b2ba03372..705be68a141e7 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -56,6 +56,7 @@ export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = { const services = { savedObjectsClient: npStart.core.savedObjects.client, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx index 25bd36829b6d0..40471b95d774c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { StepIndexPattern } from '../step_index_pattern'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; import { Header } from './components/header'; -import { IndexPatternCreationConfig } from '../../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../../plugins/index_pattern_management/public'; import { coreMock } from '../../../../../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../../../../../../plugins/data/public/mocks'; import { SavedObjectsFindResponsePublic } from '../../../../../../../../../../core/public'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index bbb6bf26e5b31..648bf7f8f9738 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -39,7 +39,7 @@ import { LoadingIndices } from './components/loading_indices'; import { StatusMessage } from './components/status_message'; import { IndicesList } from './components/indices_list'; import { Header } from './components/header'; -import { IndexPatternCreationConfig } from '../../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../../plugins/index_pattern_management/public'; import { MatchedIndex } from '../../types'; interface StepIndexPatternProps { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.test.tsx index e0c43105cb320..b23b1e3ad9051 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.test.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { IndexPatternCreationConfig } from '../../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../../plugins/index_pattern_management/public'; import { IFieldType } from '../../../../../../../../../../plugins/data/public'; import { StepTimeField } from '../step_time_field'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx index 80582cc1fbd92..a58bf10c9dab8 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx @@ -34,7 +34,7 @@ import { Header } from './components/header'; import { TimeField } from './components/time_field'; import { AdvancedOptions } from './components/advanced_options'; import { ActionButtons } from './components/action_buttons'; -import { IndexPatternCreationConfig } from '../../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../../plugins/index_pattern_management/public'; import { DataPublicPluginStart } from '../../../../../../../../../../plugins/data/public'; interface StepTimeFieldProps { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js index 50c5a58d35db3..47cb773258cb4 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js @@ -20,7 +20,6 @@ import uiRoutes from 'ui/routes'; import angularTemplate from './angular_template.html'; import { npStart } from 'ui/new_platform'; -import { setup as managementSetup } from '../../../../../../management/public/legacy'; import { getCreateBreadcrumbs } from '../breadcrumbs'; import { renderCreateIndexPatternWizard, destroyCreateIndexPatternWizard } from './render'; @@ -33,7 +32,7 @@ uiRoutes.when('/management/kibana/index_pattern', { const kbnUrl = $injector.get('kbnUrl'); $scope.$$postDigest(() => { const $routeParams = $injector.get('$routeParams'); - const indexPatternCreationType = managementSetup.indexPattern.creation.getType( + const indexPatternCreationType = npStart.plugins.indexPatternManagement.creation.getType( $routeParams.type ); const services = { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts index 5a8460fcb51ba..40583af7177fe 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts @@ -18,7 +18,7 @@ */ import { getIndices } from './get_indices'; -import { IndexPatternCreationConfig } from './../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../plugins/index_pattern_management/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public/search'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts index 3848c425e2d49..3b1b7a3b52a5b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts @@ -18,7 +18,7 @@ */ import { get, sortBy } from 'lodash'; -import { IndexPatternCreationConfig } from '../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../plugins/index_pattern_management/public'; import { DataPublicPluginStart } from '../../../../../../../../../plugins/data/public'; import { MatchedIndex } from '../types'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index 6d302ac5a74f3..594430ca01f4c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -29,7 +29,6 @@ import { uiModules } from 'ui/modules'; import template from './edit_index_pattern.html'; import { fieldWildcardMatcher } from '../../../../../../../../plugins/kibana_utils/public'; import { subscribeWithScope } from '../../../../../../../../plugins/kibana_legacy/public'; -import { setup as managementSetup } from '../../../../../../management/public/legacy'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { SourceFiltersTable } from './source_filters_table'; @@ -239,14 +238,12 @@ uiModules $scope.editSectionsProvider = Private(IndicesEditSectionsProvider); $scope.kbnUrl = Private(KbnUrlProvider); $scope.indexPattern = $route.current.locals.indexPattern; - $scope.indexPatternListProvider = managementSetup.indexPattern.list; - $scope.indexPattern.tags = managementSetup.indexPattern.list.getIndexPatternTags( + $scope.indexPatternListProvider = npStart.plugins.indexPatternManagement.list; + $scope.indexPattern.tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags( $scope.indexPattern, $scope.indexPattern.id === config.get('defaultIndex') ); - $scope.getFieldInfo = managementSetup.indexPattern.list.getFieldInfo.bind( - managementSetup.indexPattern.list - ); + $scope.getFieldInfo = npStart.plugins.indexPatternManagement.list.getFieldInfo; docTitle.change($scope.indexPattern.title); const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => { @@ -257,7 +254,7 @@ uiModules $scope.editSections = $scope.editSectionsProvider( $scope.indexPattern, $scope.fieldFilter, - managementSetup.indexPattern.list + npStart.plugins.indexPatternManagement.list ); $scope.refreshFilters(); $scope.fields = $scope.indexPattern.getNonScriptedFields(); @@ -363,7 +360,7 @@ uiModules $scope.editSections = $scope.editSectionsProvider( $scope.indexPattern, $scope.fieldFilter, - managementSetup.indexPattern.list + npStart.plugins.indexPatternManagement.list ); if ($scope.fieldFilter === undefined) { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js index 310797a7f3a0c..a8376c0e84bf9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js @@ -18,7 +18,6 @@ */ import { management } from 'ui/management'; -import { setup as managementSetup } from '../../../../../management/public/legacy'; import './create_index_pattern_wizard'; import './edit_index_pattern'; import uiRoutes from 'ui/routes'; @@ -111,7 +110,7 @@ uiModules transclude: true, template: indexTemplate, link: async function($scope) { - const indexPatternCreationOptions = await managementSetup.indexPattern.creation.getIndexPatternCreationOptions( + const indexPatternCreationOptions = await npStart.plugins.indexPatternManagement.creation.getIndexPatternCreationOptions( url => { $scope.$evalAsync(() => kbnUrl.change(url)); } @@ -124,7 +123,7 @@ uiModules const id = pattern.id; const title = pattern.get('title'); const isDefault = $scope.defaultIndex === id; - const tags = managementSetup.indexPattern.list.getIndexPatternTags( + const tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags( pattern, isDefault ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js index a5e34f8955fe3..7b9c17640a0f3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js @@ -19,7 +19,7 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../management/public/np_ready/mocks'; +import { mockManagementPlugin } from '../../../../../../../../../../plugins/index_pattern_management/public/mocks'; import { Query } from '@elastic/eui'; import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table'; @@ -30,7 +30,7 @@ import { extractExportDetails } from '../../../lib/extract_export_details'; jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); -jest.mock('../../../../../../../../management/public/legacy', () => ({ +jest.mock('../../../../../../../../../../plugins/index_pattern_management/public', () => ({ setup: mockManagementPlugin.createSetupContract(), start: mockManagementPlugin.createStartContract(), })); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js index 97c0d5b89d657..0d16e0ae35dd6 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js @@ -19,7 +19,7 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../../../management/public/np_ready/mocks'; +import { mockManagementPlugin } from '../../../../../../../../../../../../plugins/index_pattern_management/public/mocks'; import { Flyout } from '../flyout'; jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); @@ -48,7 +48,7 @@ jest.mock('../../../../../lib/resolve_saved_objects', () => ({ saveObjects: jest.fn(), })); -jest.mock('../../../../../../../../../../management/public/legacy', () => ({ +jest.mock('../../../../../../../../../../../../plugins/index_pattern_management/public', () => ({ setup: mockManagementPlugin.createSetupContract(), start: mockManagementPlugin.createStartContract(), })); @@ -519,7 +519,8 @@ describe('Flyout', () => { expect(resolveIndexPatternConflicts).toHaveBeenCalledWith( component.instance().resolutions, mockConflictedIndexPatterns, - true + true, + defaultProps.indexPatterns ); expect(saveObjects).toHaveBeenCalledWith( mockConflictedSavedObjectsLinkedToSavedSearches, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js index 105c279218375..da2221bb54203 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js @@ -358,7 +358,8 @@ export class Flyout extends Component { importCount += await resolveIndexPatternConflicts( resolutions, conflictedIndexPatterns, - isOverwriteAllChecked + isOverwriteAllChecked, + this.props.indexPatterns ); } this.setState({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts index 8243aa69ac082..dc6d2643145ff 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts @@ -84,7 +84,7 @@ describe('resolveSavedObjects', () => { }, } as unknown) as IndexPatternsContract; - const services = [ + const services = ([ { type: 'search', get: async () => { @@ -124,7 +124,7 @@ describe('resolveSavedObjects', () => { }; }, }, - ] as SavedObjectLoader[]; + ] as unknown) as SavedObjectLoader[]; const overwriteAll = false; @@ -176,7 +176,7 @@ describe('resolveSavedObjects', () => { }, } as unknown) as IndexPatternsContract; - const services = [ + const services = ([ { type: 'search', get: async () => { @@ -217,7 +217,7 @@ describe('resolveSavedObjects', () => { }; }, }, - ] as SavedObjectLoader[]; + ] as unknown) as SavedObjectLoader[]; const overwriteAll = false; @@ -237,33 +237,38 @@ describe('resolveSavedObjects', () => { describe('resolveIndexPatternConflicts', () => { it('should resave resolutions', async () => { - const hydrateIndexPattern = jest.fn(); const save = jest.fn(); - const conflictedIndexPatterns = [ + const conflictedIndexPatterns = ([ { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '1' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '1', + }), }, }, - hydrateIndexPattern, - save, }, }, { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '3' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '3', + }), }, }, - hydrateIndexPattern, - save, }, }, - ]; + ] as unknown) as Array<{ obj: SavedObject; doc: any }>; const resolutions = [ { @@ -282,43 +287,49 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll); - expect(hydrateIndexPattern.mock.calls.length).toBe(2); + await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract); + expect(conflictedIndexPatterns[0].obj.searchSource!.getField('index')!.id).toEqual('2'); + expect(conflictedIndexPatterns[1].obj.searchSource!.getField('index')!.id).toEqual('4'); expect(save.mock.calls.length).toBe(2); expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); - expect(hydrateIndexPattern).toHaveBeenCalledWith('2'); - expect(hydrateIndexPattern).toHaveBeenCalledWith('4'); }); it('should resolve filter index conflicts', async () => { - const hydrateIndexPattern = jest.fn(); const save = jest.fn(); - const conflictedIndexPatterns = [ + const conflictedIndexPatterns = ([ { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '1' : [{ meta: { index: 'filterIndex' } }]; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '1', + filter: [{ meta: { index: 'filterIndex' } }], + }), }, - setField: jest.fn(), }, - hydrateIndexPattern, - save, }, }, { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '3' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '3', + }), }, }, - hydrateIndexPattern, - save, }, }, - ]; + ] as unknown) as Array<{ obj: SavedObject; doc: any }>; const resolutions = [ { @@ -337,9 +348,11 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll); + await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract); - expect(conflictedIndexPatterns[0].obj.searchSource.setField).toHaveBeenCalledWith('filter', [ + expect(conflictedIndexPatterns[0].obj.searchSource!.getField('filter')).toEqual([ { meta: { index: 'newFilterIndex' } }, ]); expect(save.mock.calls.length).toBe(2); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts index 902de654f5f85..d9473367f7502 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts @@ -18,12 +18,17 @@ */ import { i18n } from '@kbn/i18n'; -import { OverlayStart } from 'src/core/public'; +import { cloneDeep } from 'lodash'; +import { OverlayStart, SavedObjectReference } from 'src/core/public'; import { SavedObject, SavedObjectLoader, } from '../../../../../../../../plugins/saved_objects/public'; -import { IndexPatternsContract, IIndexPattern } from '../../../../../../../../plugins/data/public'; +import { + IndexPatternsContract, + IIndexPattern, + createSearchSource, +} from '../../../../../../../../plugins/data/public'; type SavedObjectsRawDoc = Record; @@ -126,7 +131,7 @@ async function importIndexPattern( async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) { await obj.applyESResp({ references: doc._references || [], - ...doc, + ...cloneDeep(doc), }); return await obj.save({ confirmOverwrite: !overwriteAll }); } @@ -160,41 +165,57 @@ async function awaitEachItemInParallel(list: T[], op: (item: T) => R) { export async function resolveIndexPatternConflicts( resolutions: Array<{ oldId: string; newId: string }>, conflictedIndexPatterns: any[], - overwriteAll: boolean + overwriteAll: boolean, + indexPatterns: IndexPatternsContract ) { let importCount = 0; - await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj }) => { - // Resolve search index reference: - let oldIndexId = obj.searchSource.getOwnField('index'); - // Depending on the object, this can either be the raw id or the actual index pattern object - if (typeof oldIndexId !== 'string') { - oldIndexId = oldIndexId.id; - } - let resolution = resolutions.find(({ oldId }) => oldId === oldIndexId); - if (resolution) { - const newIndexId = resolution.newId; - await obj.hydrateIndexPattern(newIndexId); + await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj, doc }) => { + const serializedSearchSource = JSON.parse( + doc._source.kibanaSavedObjectMeta?.searchSourceJSON || '{}' + ); + const oldIndexId = serializedSearchSource.index; + let allResolved = true; + const inlineResolution = resolutions.find(({ oldId }) => oldId === oldIndexId); + if (inlineResolution) { + serializedSearchSource.index = inlineResolution.newId; + } else { + allResolved = false; } // Resolve filter index reference: - const filter = (obj.searchSource.getOwnField('filter') || []).map((f: any) => { + const filter = (serializedSearchSource.filter || []).map((f: any) => { if (!(f.meta && f.meta.index)) { return f; } - resolution = resolutions.find(({ oldId }) => oldId === f.meta.index); + const resolution = resolutions.find(({ oldId }) => oldId === f.meta.index); return resolution ? { ...f, ...{ meta: { ...f.meta, index: resolution.newId } } } : f; }); if (filter.length > 0) { - obj.searchSource.setField('filter', filter); + serializedSearchSource.filter = filter; } - if (!resolution) { + const replacedReferences = (doc._references || []).map((reference: SavedObjectReference) => { + const resolution = resolutions.find(({ oldId }) => oldId === reference.id); + if (resolution) { + return { ...reference, id: resolution.newId }; + } else { + allResolved = false; + } + + return reference; + }); + + if (!allResolved) { // The user decided to skip this conflict so do nothing return; } + obj.searchSource = await createSearchSource(indexPatterns)( + JSON.stringify(serializedSearchSource), + replacedReferences + ); if (await saveObject(obj, overwriteAll)) { importCount++; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index 7c9ab32ab2f72..a710d3e318749 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -71,6 +71,7 @@ const getResolvedResults = deps => { return createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, + search: data.search, chrome: core.chrome, overlays: core.overlays, }).get(results.vis.data.savedSearchId); diff --git a/src/legacy/core_plugins/management/index.ts b/src/legacy/core_plugins/management/index.ts deleted file mode 100644 index 4962c948f842f..0000000000000 --- a/src/legacy/core_plugins/management/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { Legacy } from '../../../../kibana'; - -// eslint-disable-next-line import/no-default-export -export default function ManagementPlugin(kibana: any) { - const config: Legacy.PluginSpecOptions = { - id: 'stack-management', - publicDir: resolve(__dirname, 'public'), - config: (Joi: any) => { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - init: (server: Legacy.Server) => ({}), - }; - - return new kibana.Plugin(config); -} diff --git a/src/legacy/core_plugins/management/package.json b/src/legacy/core_plugins/management/package.json deleted file mode 100644 index 77d33a7bce3b6..0000000000000 --- a/src/legacy/core_plugins/management/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "management", - "version": "kibana" -} - \ No newline at end of file diff --git a/src/legacy/core_plugins/management/public/legacy.ts b/src/legacy/core_plugins/management/public/legacy.ts deleted file mode 100644 index 96d2c74398a0e..0000000000000 --- a/src/legacy/core_plugins/management/public/legacy.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * New Platform Shim - * - * In this file, we import any legacy dependencies we have, and shim them into - * our plugin by manually constructing the values that the new platform will - * eventually be passing to the `setup/start` method of our plugin definition. - * - * The idea is that our `plugin.ts` can stay "pure" and not contain any legacy - * world code. Then when it comes time to migrate to the new platform, we can - * simply delete this shim file. - * - * We are also calling `setup/start` here and exporting our public contract so that - * other legacy plugins are able to import from '../core_plugins/management/legacy' - * and receive the response value of the `setup/start` contract, mimicking the - * data that will eventually be injected by the new platform. - */ - -import { PluginInitializerContext } from 'src/core/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { plugin } from '.'; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, { home: npSetup.plugins.home }); -export const start = pluginInstance.start(npStart.core, {}); diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts b/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts index 201b21f932988..e7f431a178ea0 100644 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts +++ b/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts @@ -28,6 +28,7 @@ const savedObjectsClient = npStart.core.savedObjects.client; const services = { savedObjectsClient, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/src/legacy/core_plugins/vis_type_metric/public/services.ts b/src/legacy/core_plugins/vis_type_metric/public/services.ts index 5af11bc7f0b03..b303ccd5aeed2 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/services.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; export const [getFormatService, setFormatService] = createGetterSetter< diff --git a/src/legacy/core_plugins/vis_type_table/public/services.ts b/src/legacy/core_plugins/vis_type_table/public/services.ts index 08efed733cafe..b4b491ac7a555 100644 --- a/src/legacy/core_plugins/vis_type_table/public/services.ts +++ b/src/legacy/core_plugins/vis_type_table/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; export const [getFormatService, setFormatService] = createGetterSetter< diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts index fef46282eb8dd..272bed3e91a08 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; export const [getFormatService, setFormatService] = createGetterSetter< diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts index 30c62d778933b..1db35c406eb13 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts @@ -25,7 +25,7 @@ import { metricsRequestHandler } from './request_handler'; import { EditorController } from './editor_controller'; // @ts-ignore import { PANEL_TYPES } from '../../../../plugins/vis_type_timeseries/common/panel_types'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; export const metricsVisDefinition = { name: 'metrics', diff --git a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts b/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts index 64a9aaaf3b7a6..b2f3e5b2241e6 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts index 5d9ed5c8df91c..f56d7682efc6f 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; import { createVegaRequestHandler } from './vega_request_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts index da16a38deba9f..c04ffa506eb04 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts @@ -19,8 +19,3 @@ import { search } from '../../../../plugins/data/public'; export const { tabifyAggResponse, tabifyGetColumns } = search; - -// @ts-ignore -export { buildHierarchicalData } from 'ui/agg_response/hierarchical/build_hierarchical_data'; -// @ts-ignore -export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/services.ts b/src/legacy/core_plugins/vis_type_vislib/public/services.ts index da50e227d84d2..0d6b1b5e8de58 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/services.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; export const [getDataActions, setDataActions] = createGetterSetter< diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js deleted file mode 100644 index 3574fb232883d..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { aggResponseIndex } from 'ui/agg_response'; - -import { vislibSeriesResponseHandler } from '../response_handler'; - -/** - * TODO: Fix these tests if still needed - * - * All these tests were not being run in master or prodiced false positive results - * Fixing them would require changes to the response handler logic. - */ - -describe.skip('Basic Response Handler', function() { - beforeEach(ngMock.module('kibana')); - - it('returns empty object if conversion failed', () => { - const data = vislibSeriesResponseHandler({}); - expect(data).to.not.be.an('undefined'); - expect(data).to.equal({}); - }); - - it('returns empty object if no data was found', () => { - const data = vislibSeriesResponseHandler({ - columns: [{ id: '1', title: '1', aggConfig: {} }], - rows: [], - }); - expect(data).to.not.be.an('undefined'); - expect(data.rows).to.equal([]); - }); -}); - -describe.skip('renderbot#buildChartData', function() { - describe('for hierarchical vis', function() { - it('defers to hierarchical aggResponse converter', function() { - const football = {}; - const stub = sinon.stub(aggResponseIndex, 'hierarchical').returns(football); - expect(vislibSeriesResponseHandler(football)).to.be(football); - expect(stub).to.have.property('callCount', 1); - expect(stub.firstCall.args[1]).to.be(football); - }); - }); - - describe('for point plot', function() { - it('calls tabify to simplify the data into a table', function() { - const football = { tables: [], hits: { total: 1 } }; - const stub = sinon.stub(aggResponseIndex, 'tabify').returns(football); - expect(vislibSeriesResponseHandler(football)).to.eql({ rows: [], hits: 1 }); - expect(stub).to.have.property('callCount', 1); - expect(stub.firstCall.args[1]).to.be(football); - }); - - it('returns a single chart if the tabify response contains only a single table', function() { - const chart = { hits: 1, rows: [], columns: [] }; - const esResp = { hits: { total: 1 } }; - const tabbed = { tables: [{}] }; - - sinon.stub(aggResponseIndex, 'tabify').returns(tabbed); - expect(vislibSeriesResponseHandler(esResp)).to.eql(chart); - }); - - it('converts table groups into rows/columns wrappers for charts', function() { - const converter = sinon.stub().returns('chart'); - const esResp = { hits: { total: 1 } }; - const tables = [{}, {}, {}, {}]; - - sinon.stub(aggResponseIndex, 'tabify').returns({ - tables: [ - { - aggConfig: { params: { row: true } }, - tables: [ - { - aggConfig: { params: { row: false } }, - tables: [tables[0]], - }, - { - aggConfig: { params: { row: false } }, - tables: [tables[1]], - }, - ], - }, - { - aggConfig: { params: { row: true } }, - tables: [ - { - aggConfig: { params: { row: false } }, - tables: [tables[2]], - }, - { - aggConfig: { params: { row: false } }, - tables: [tables[3]], - }, - ], - }, - ], - }); - - const chartData = vislibSeriesResponseHandler(esResp); - - // verify tables were converted - expect(converter).to.have.property('callCount', 4); - expect(converter.args[0][1]).to.be(tables[0]); - expect(converter.args[1][1]).to.be(tables[1]); - expect(converter.args[2][1]).to.be(tables[2]); - expect(converter.args[3][1]).to.be(tables[3]); - - expect(chartData).to.have.property('rows'); - expect(chartData.rows).to.have.length(2); - chartData.rows.forEach(function(row) { - expect(row).to.have.property('columns'); - expect(row.columns).to.eql(['chart', 'chart']); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts similarity index 80% rename from src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts index 21a937bf1fb66..475555f3a15f3 100644 --- a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts @@ -17,24 +17,26 @@ * under the License. */ -import { buildHierarchicalData } from './build_hierarchical_data'; +import { buildHierarchicalData, Dimensions, Dimension } from './build_hierarchical_data'; +import { Table, TableParent } from '../../types'; -function tableVisResponseHandler(table, dimensions) { - const converted = { +function tableVisResponseHandler(table: Table, dimensions: Dimensions) { + const converted: { + tables: Array; + } = { tables: [], }; const split = dimensions.splitColumn || dimensions.splitRow; if (split) { - converted.direction = dimensions.splitRow ? 'row' : 'column'; const splitColumnIndex = split[0].accessor; const splitColumn = table.columns[splitColumnIndex]; - const splitMap = {}; + const splitMap: { [key: string]: number } = {}; let splitIndex = 0; table.rows.forEach((row, rowIndex) => { - const splitValue = row[splitColumn.id]; + const splitValue = row[splitColumn.id] as string; if (!splitMap.hasOwnProperty(splitValue)) { splitMap[splitValue] = splitIndex++; @@ -46,8 +48,8 @@ function tableVisResponseHandler(table, dimensions) { column: splitColumnIndex, row: rowIndex, table, - tables: [], - }; + tables: [] as Table[], + } as any; tableGroup.tables.push({ $parent: tableGroup, @@ -59,34 +61,30 @@ function tableVisResponseHandler(table, dimensions) { } const tableIndex = splitMap[splitValue]; - converted.tables[tableIndex].tables[0].rows.push(row); + (converted.tables[tableIndex] as TableParent).tables![0].rows.push(row); }); } else { converted.tables.push({ columns: table.columns, rows: table.rows, - }); + } as Table); } return converted; } -jest.mock('ui/new_platform'); -jest.mock('ui/chrome', () => ({ - getUiSettingsClient: jest.fn().mockReturnValue({ - get: jest.fn().mockReturnValue('KQL'), - }), -})); -jest.mock('ui/visualize/loader/pipeline_helpers/utilities', () => ({ - getFormat: jest.fn(() => ({ - convert: jest.fn(v => v), +jest.mock('../../../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: () => ({ + convert: jest.fn(v => JSON.stringify(v)), + }), })), })); describe('buildHierarchicalData convertTable', () => { describe('metric only', () => { - let dimensions; - let table; + let dimensions: Dimensions; + let table: Table; beforeEach(() => { const tabifyResponse = { @@ -94,11 +92,11 @@ describe('buildHierarchicalData convertTable', () => { rows: [{ 'col-0-agg_1': 412032 }], }; dimensions = { - metric: { accessor: 0 }, + metric: { accessor: 0 } as Dimension, }; const tableGroup = tableVisResponseHandler(tabifyResponse, dimensions); - table = tableGroup.tables[0]; + table = tableGroup.tables[0] as Table; }); it('should set the slices with one child to a consistent label', () => { @@ -118,8 +116,8 @@ describe('buildHierarchicalData convertTable', () => { }); describe('threeTermBuckets', () => { - let dimensions; - let tables; + let dimensions: Dimensions; + let tables: TableParent[]; beforeEach(async () => { const tabifyResponse = { @@ -231,60 +229,60 @@ describe('buildHierarchicalData convertTable', () => { ], }; dimensions = { - splitRow: [{ accessor: 0 }], - metric: { accessor: 5 }, - buckets: [{ accessor: 2 }, { accessor: 4 }], + splitRow: [{ accessor: 0 } as Dimension], + metric: { accessor: 5 } as Dimension, + buckets: [{ accessor: 2 }, { accessor: 4 }] as Dimension[], }; const tableGroup = await tableVisResponseHandler(tabifyResponse, dimensions); - tables = tableGroup.tables; + tables = tableGroup.tables as TableParent[]; }); it('should set the correct hits attribute for each of the results', () => { tables.forEach(t => { - const results = buildHierarchicalData(t.tables[0], dimensions); + const results = buildHierarchicalData(t.tables![0], dimensions); expect(results).toHaveProperty('hits'); expect(results.hits).toBe(4); }); }); it('should set the correct names for each of the results', () => { - const results0 = buildHierarchicalData(tables[0].tables[0], dimensions); + const results0 = buildHierarchicalData(tables[0].tables![0], dimensions); expect(results0).toHaveProperty('names'); expect(results0.names).toHaveLength(5); - const results1 = buildHierarchicalData(tables[1].tables[0], dimensions); + const results1 = buildHierarchicalData(tables[1].tables![0], dimensions); expect(results1).toHaveProperty('names'); expect(results1.names).toHaveLength(5); - const results2 = buildHierarchicalData(tables[2].tables[0], dimensions); + const results2 = buildHierarchicalData(tables[2].tables![0], dimensions); expect(results2).toHaveProperty('names'); expect(results2.names).toHaveLength(4); }); it('should set the parent of the first item in the split', () => { - const results0 = buildHierarchicalData(tables[0].tables[0], dimensions); + const results0 = buildHierarchicalData(tables[0].tables![0], dimensions); expect(results0).toHaveProperty('slices'); expect(results0.slices).toHaveProperty('children'); expect(results0.slices.children).toHaveLength(2); - expect(results0.slices.children[0].rawData.table.$parent).toHaveProperty('key', 'png'); + expect(results0.slices.children[0].rawData!.table.$parent).toHaveProperty('key', 'png'); - const results1 = buildHierarchicalData(tables[1].tables[0], dimensions); + const results1 = buildHierarchicalData(tables[1].tables![0], dimensions); expect(results1).toHaveProperty('slices'); expect(results1.slices).toHaveProperty('children'); expect(results1.slices.children).toHaveLength(2); - expect(results1.slices.children[0].rawData.table.$parent).toHaveProperty('key', 'css'); + expect(results1.slices.children[0].rawData!.table.$parent).toHaveProperty('key', 'css'); - const results2 = buildHierarchicalData(tables[2].tables[0], dimensions); + const results2 = buildHierarchicalData(tables[2].tables![0], dimensions); expect(results2).toHaveProperty('slices'); expect(results2.slices).toHaveProperty('children'); expect(results2.slices.children).toHaveLength(2); - expect(results2.slices.children[0].rawData.table.$parent).toHaveProperty('key', 'html'); + expect(results2.slices.children[0].rawData!.table.$parent).toHaveProperty('key', 'html'); }); }); describe('oneHistogramBucket', () => { - let dimensions; - let table; + let dimensions: Dimensions; + let table: Table; beforeEach(async () => { const tabifyResponse = { @@ -302,11 +300,11 @@ describe('buildHierarchicalData convertTable', () => { ], }; dimensions = { - metric: { accessor: 1 }, - buckets: [{ accessor: 0, params: { field: 'bytes', interval: 8192 } }], + metric: { accessor: 1 } as Dimension, + buckets: [{ accessor: 0 } as Dimension], }; const tableGroup = await tableVisResponseHandler(tabifyResponse, dimensions); - table = tableGroup.tables[0]; + table = tableGroup.tables[0] as Table; }); it('should set the hits attribute for the results', () => { @@ -320,8 +318,8 @@ describe('buildHierarchicalData convertTable', () => { }); describe('oneRangeBucket', () => { - let dimensions; - let table; + let dimensions: Dimensions; + let table: Table; beforeEach(async () => { const tabifyResponse = { @@ -335,11 +333,11 @@ describe('buildHierarchicalData convertTable', () => { ], }; dimensions = { - metric: { accessor: 1 }, - buckets: [{ accessor: 0, format: { id: 'range', params: { id: 'agg_2' } } }], + metric: { accessor: 1 } as Dimension, + buckets: [{ accessor: 0, format: { id: 'range', params: { id: 'agg_2' } } } as Dimension], }; const tableGroup = await tableVisResponseHandler(tabifyResponse, dimensions); - table = tableGroup.tables[0]; + table = tableGroup.tables[0] as Table; }); it('should set the hits attribute for the results', () => { @@ -348,13 +346,13 @@ describe('buildHierarchicalData convertTable', () => { expect(results).toHaveProperty('slices'); expect(results.slices).toHaveProperty('children'); expect(results).toHaveProperty('names'); - // expect(results.names).toHaveLength(2); + expect(results.names).toHaveLength(2); }); }); describe('oneFilterBucket', () => { - let dimensions; - let table; + let dimensions: Dimensions; + let table: Table; beforeEach(async () => { const tabifyResponse = { @@ -368,15 +366,15 @@ describe('buildHierarchicalData convertTable', () => { ], }; dimensions = { - metric: { accessor: 1 }, + metric: { accessor: 1 } as Dimension, buckets: [ { accessor: 0, }, - ], + ] as Dimension[], }; const tableGroup = await tableVisResponseHandler(tabifyResponse, dimensions); - table = tableGroup.tables[0]; + table = tableGroup.tables[0] as Table; }); it('should set the hits attribute for the results', () => { diff --git a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts similarity index 63% rename from src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts index dcc27e956b3f8..2c6d62ed084b5 100644 --- a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts @@ -18,11 +18,41 @@ */ import { toArray } from 'lodash'; -import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; +import { SerializedFieldFormat } from '../../../../../../../plugins/expressions/common/types'; +import { getFormatService } from '../../../services'; +import { Table } from '../../types'; -export const buildHierarchicalData = (table, { metric, buckets = [] }) => { - let slices; - const names = {}; +export interface Dimension { + accessor: number; + format: { + id?: string; + params?: SerializedFieldFormat; + }; +} + +export interface Dimensions { + metric: Dimension; + buckets?: Dimension[]; + splitRow?: Dimension[]; + splitColumn?: Dimension[]; +} + +interface Slice { + name: string; + size: number; + parent?: Slice; + children?: []; + rawData?: { + table: Table; + row: number; + column: number; + value: string | number | object; + }; +} + +export const buildHierarchicalData = (table: Table, { metric, buckets = [] }: Dimensions) => { + let slices: Slice[]; + const names: { [key: string]: string } = {}; const metricColumn = table.columns[metric.accessor]; const metricFieldFormatter = metric.format; @@ -30,25 +60,25 @@ export const buildHierarchicalData = (table, { metric, buckets = [] }) => { slices = [ { name: metricColumn.name, - size: table.rows[0][metricColumn.id], + size: table.rows[0][metricColumn.id] as number, }, ]; names[metricColumn.name] = metricColumn.name; } else { slices = []; table.rows.forEach((row, rowIndex) => { - let parent; + let parent: Slice; let dataLevel = slices; buckets.forEach(bucket => { const bucketColumn = table.columns[bucket.accessor]; const bucketValueColumn = table.columns[bucket.accessor + 1]; - const bucketFormatter = getFormat(bucket.format); + const bucketFormatter = getFormatService().deserialize(bucket.format); const name = bucketFormatter.convert(row[bucketColumn.id]); - const size = row[bucketValueColumn.id]; + const size = row[bucketValueColumn.id] as number; names[name] = name; - let slice = dataLevel.find(slice => slice.name === name); + let slice = dataLevel.find(dataLevelSlice => dataLevelSlice.name === name); if (!slice) { slice = { name, @@ -66,7 +96,7 @@ export const buildHierarchicalData = (table, { metric, buckets = [] }) => { } parent = slice; - dataLevel = slice.children; + dataLevel = slice.children as []; }); }); } diff --git a/src/legacy/ui/public/agg_response/index.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts similarity index 70% rename from src/legacy/ui/public/agg_response/index.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts index 982c1c25a8101..90924e79f6027 100644 --- a/src/legacy/ui/public/agg_response/index.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts @@ -17,12 +17,5 @@ * under the License. */ -import { buildHierarchicalData } from './hierarchical/build_hierarchical_data'; -import { buildPointSeriesData } from './point_series/point_series'; -import { search } from '../../../../plugins/data/public'; - -export const aggResponseIndex = { - hierarchical: buildHierarchicalData, - pointSeries: buildPointSeriesData, - tabify: search.tabifyAggResponse, -}; +export { buildPointSeriesData } from './point_series'; +export { buildHierarchicalData } from './hierarchical/build_hierarchical_data'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts new file mode 100644 index 0000000000000..e4fdd6bb71c00 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { addToSiri, Serie } from './_add_to_siri'; +import { Point } from './_get_point'; +import { Dimension } from './point_series'; + +describe('addToSiri', function() { + it('creates a new series the first time it sees an id', function() { + const series = new Map(); + const point = {} as Point; + const id = 'id'; + addToSiri(series, point, id, id, { id }); + + const expectedSerie = series.get(id) as Serie; + expect(series.has(id)).toBe(true); + expect(expectedSerie).toEqual(expect.any(Object)); + expect(expectedSerie.label).toBe(id); + expect(expectedSerie.values).toHaveLength(1); + expect(expectedSerie.values[0]).toBe(point); + }); + + it('adds points to existing series if id has been seen', function() { + const series = new Map(); + const id = 'id'; + + const point = {} as Point; + addToSiri(series, point, id, id, { id }); + + const point2 = {} as Point; + addToSiri(series, point2, id, id, { id }); + + expect(series.has(id)).toBe(true); + expect(series.get(id)).toEqual(expect.any(Object)); + expect(series.get(id).label).toBe(id); + expect(series.get(id).values).toHaveLength(2); + expect(series.get(id).values[0]).toBe(point); + expect(series.get(id).values[1]).toBe(point2); + }); + + it('allows overriding the series label', function() { + const series = new Map(); + const id = 'id'; + const label = 'label'; + const point = {} as Point; + addToSiri(series, point, id, label, { id }); + + expect(series.has(id)).toBe(true); + expect(series.get(id)).toEqual(expect.any(Object)); + expect(series.get(id).label).toBe(label); + expect(series.get(id).values).toHaveLength(1); + expect(series.get(id).values[0]).toBe(point); + }); + + it('correctly sets id and rawId', function() { + const series = new Map(); + const id = 'id-id2'; + + const point = {} as Point; + addToSiri(series, point, id, undefined, {} as Dimension['format']); + + expect(series.has(id)).toBe(true); + expect(series.get(id)).toEqual(expect.any(Object)); + expect(series.get(id).label).toBe(id); + expect(series.get(id).rawId).toBe(id); + expect(series.get(id).id).toBe('id2'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts new file mode 100644 index 0000000000000..5e5185d6c31ab --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Point } from './_get_point'; +import { Dimension } from './point_series'; + +export interface Serie { + id: string; + rawId: string; + label: string; + count: number; + values: Point[]; + format: Dimension['format']; + zLabel?: string; + zFormat?: Dimension['format']; +} + +export function addToSiri( + series: Map, + point: Point, + id: string, + yLabel: string | undefined | null, + yFormat: Dimension['format'], + zFormat?: Dimension['format'], + zLabel?: string +) { + id = id == null ? '' : id + ''; + + if (series.has(id)) { + (series.get(id) as Serie).values.push(point); + return; + } + + series.set(id, { + id: id.split('-').pop() as string, + rawId: id, + label: yLabel == null ? id : yLabel, + count: 0, + values: [point], + format: yFormat, + zLabel, + zFormat, + }); +} diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts similarity index 74% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts index 6c246d7f50897..43d4c3d7ca7c4 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts @@ -16,20 +16,17 @@ * specific language governing permissions and limitations * under the License. */ - -import expect from '@kbn/expect'; -import { makeFakeXAspect } from '../_fake_x_aspect'; +import { makeFakeXAspect } from './_fake_x_aspect'; describe('makeFakeXAspect', function() { it('creates an object that looks like an aspect', function() { const aspect = makeFakeXAspect(); - expect(aspect) - .to.have.property('accessor', -1) - .and.have.property('title', 'All docs') - .and.have.property('format') - .and.have.property('params'); + expect(aspect).toHaveProperty('accessor', -1); + expect(aspect).toHaveProperty('title', 'All docs'); + expect(aspect).toHaveProperty('format'); + expect(aspect).toHaveProperty('params'); - expect(aspect.params).to.have.property('defaultValue', '_all'); + expect(aspect.params).toHaveProperty('defaultValue', '_all'); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_fake_x_aspect.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts similarity index 88% rename from src/legacy/ui/public/agg_response/point_series/_fake_x_aspect.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts index 254a42baeddb0..1bffa4cceb5b0 100644 --- a/src/legacy/ui/public/agg_response/point_series/_fake_x_aspect.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts @@ -18,16 +18,17 @@ */ import { i18n } from '@kbn/i18n'; +import { Aspect } from './point_series'; export function makeFakeXAspect() { return { accessor: -1, - title: i18n.translate('common.ui.aggResponse.allDocsTitle', { + title: i18n.translate('visTypeVislib.aggResponse.allDocsTitle', { defaultMessage: 'All docs', }), params: { defaultValue: '_all', }, format: {}, - }; + } as Aspect; } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_aspects.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts similarity index 53% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_get_aspects.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts index fab5c2e290e7e..450b283abbed2 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_aspects.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts @@ -17,37 +17,37 @@ * under the License. */ -import expect from '@kbn/expect'; -import { getAspects } from '../_get_aspects'; +import { getAspects } from './_get_aspects'; +import { Dimension, Dimensions, Aspect } from './point_series'; +import { Table, Row } from '../../types'; describe('getAspects', function() { - let table; - let dimensions; + let table: Table; + let dimensions: Dimensions; - function validate(aspect, i) { - expect(aspect) - .to.be.an('object') - .and.have.property('accessor', i); + function validate(aspect: Aspect, i: string) { + expect(aspect).toEqual(expect.any(Object)); + expect(aspect).toHaveProperty('accessor', i); } - function init(group, x, y) { + function init(group: number, x: number | null, y: number) { table = { columns: [ - { id: '0', title: 'date' }, // date - { id: '1', title: 'date utc_time' }, // date - { id: '2', title: 'ext' }, // extension - { id: '3', title: 'geo.src' }, // extension - { id: '4', title: 'count' }, // count - { id: '5', title: 'avg bytes' }, // avg + { id: '0', name: 'date' }, // date + { id: '1', name: 'date utc_time' }, // date + { id: '2', name: 'ext' }, // extension + { id: '3', name: 'geo.src' }, // extension + { id: '4', name: 'count' }, // count + { id: '5', name: 'avg bytes' }, // avg ], - rows: [], - }; + rows: [] as Row[], + } as Table; dimensions = { - x: { accessor: x }, - y: { accessor: y }, - series: { accessor: group }, - }; + x: { accessor: x } as Dimension, + y: [{ accessor: y } as Dimension], + series: [{ accessor: group } as Dimension], + } as Dimensions; } it('produces an aspect object for each of the aspect types found in the columns', function() { @@ -55,8 +55,8 @@ describe('getAspects', function() { const aspects = getAspects(table, dimensions); validate(aspects.x[0], '0'); - validate(aspects.series[0], '1'); - validate(aspects.y[0], '2'); + validate(aspects.series![0], '1'); + validate(aspects.y![0], '2'); }); it('creates a fake x aspect if the column does not exist', function() { @@ -64,9 +64,8 @@ describe('getAspects', function() { const aspects = getAspects(table, dimensions); - expect(aspects.x[0]) - .to.be.an('object') - .and.have.property('accessor', -1) - .and.have.property('title'); + expect(aspects.x[0]).toEqual(expect.any(Object)); + expect(aspects.x[0]).toHaveProperty('accessor', -1); + expect(aspects.x[0]).toHaveProperty('title'); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_get_aspects.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts similarity index 72% rename from src/legacy/ui/public/agg_response/point_series/_get_aspects.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts index fe74d8566c0e7..29134409ddd5f 100644 --- a/src/legacy/ui/public/agg_response/point_series/_get_aspects.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts @@ -18,20 +18,22 @@ */ import { makeFakeXAspect } from './_fake_x_aspect'; +import { Dimensions, Aspects } from './point_series'; +import { Table } from '../../types'; /** * Identify and group the columns based on the aspect of the pointSeries * they represent. * - * @param {array} columns - the list of columns * @return {object} - an object with a key for each aspect (see map). The values - * may be undefined, a single aspect, or an array of aspects. + * may be undefined or an array of aspects. */ -export function getAspects(table, dimensions) { - const aspects = {}; - Object.keys(dimensions).forEach(name => { - const dimension = Array.isArray(dimensions[name]) ? dimensions[name] : [dimensions[name]]; - dimension.forEach(d => { +export function getAspects(table: Table, dimensions: Dimensions) { + const aspects: Partial = {}; + (Object.keys(dimensions) as Array).forEach(name => { + const dimension = dimensions[name]; + const dimensionList = Array.isArray(dimension) ? dimension : [dimension]; + dimensionList.forEach(d => { if (!d) { return; } @@ -42,7 +44,7 @@ export function getAspects(table, dimensions) { if (!aspects[name]) { aspects[name] = []; } - aspects[name].push({ + aspects[name]!.push({ accessor: column.id, column: d.accessor, title: column.name, @@ -56,5 +58,5 @@ export function getAspects(table, dimensions) { aspects.x = [makeFakeXAspect()]; } - return aspects; + return aspects as Aspects; } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts new file mode 100644 index 0000000000000..0c79c5b263cea --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IFieldFormatsRegistry } from '../../../../../../../plugins/data/common'; +import { getPoint } from './_get_point'; +import { setFormatService } from '../../../services'; +import { Aspect } from './point_series'; +import { Table, Row, Column } from '../../types'; + +describe('getPoint', function() { + let deserialize: IFieldFormatsRegistry['deserialize']; + + beforeAll(() => { + deserialize = jest.fn(() => ({ + convert: jest.fn(v => v), + })) as any; + + setFormatService({ + deserialize, + } as any); + }); + + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 1, '1': 2, '2': 3 }, + { '0': 4, '1': 'NaN', '2': 6 }, + ], + } as Table; + + describe('Without series aspect', function() { + let seriesAspect: undefined; + let xAspect: Aspect; + let yAspect: Aspect; + + beforeEach(function() { + xAspect = { accessor: '0' } as Aspect; + yAspect = { accessor: '1', title: 'Y' } as Aspect; + }); + + it('properly unwraps values', function() { + const row = table.rows[0]; + const zAspect = { accessor: '2' } as Aspect; + const point = getPoint(table, xAspect, seriesAspect, row, 0, yAspect, zAspect); + + expect(point).toHaveProperty('x', 1); + expect(point).toHaveProperty('y', 2); + expect(point).toHaveProperty('z', 3); + expect(point).toHaveProperty('series', yAspect.title); + }); + + it('ignores points with a y value of NaN', function() { + const row = table.rows[1]; + const point = getPoint(table, xAspect, seriesAspect, row, 1, yAspect); + expect(point).toBe(void 0); + }); + }); + + describe('With series aspect', function() { + let row: Row; + let xAspect: Aspect; + let yAspect: Aspect; + + beforeEach(function() { + row = table.rows[0]; + xAspect = { accessor: '0' } as Aspect; + yAspect = { accessor: '2' } as Aspect; + }); + + it('properly unwraps values', function() { + const seriesAspect = [{ accessor: '1' } as Aspect]; + const point = getPoint(table, xAspect, seriesAspect, row, 0, yAspect); + + expect(point).toHaveProperty('x', 1); + expect(point).toHaveProperty('series', '2'); + expect(point).toHaveProperty('y', 3); + }); + + it('should call deserialize', function() { + const seriesAspect = [ + { accessor: '1', format: { id: 'number', params: { pattern: '$' } } } as Aspect, + ]; + getPoint(table, xAspect, seriesAspect, row, 0, yAspect); + + expect(deserialize).toHaveBeenCalledWith(seriesAspect[0].format); + }); + }); +}); diff --git a/src/legacy/ui/public/agg_response/point_series/_get_point.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts similarity index 69% rename from src/legacy/ui/public/agg_response/point_series/_get_point.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts index 11e639f3f54a8..3fc13eb0c04b5 100644 --- a/src/legacy/ui/public/agg_response/point_series/_get_point.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts @@ -17,19 +17,55 @@ * under the License. */ -import { getFormat } from '../../visualize/loader/pipeline_helpers/utilities'; +import { getFormatService } from '../../../services'; +import { Aspect } from './point_series'; +import { Table, Row } from '../../types'; -export function getPoint(table, x, series, yScale, row, rowIndex, y, z) { +type RowValue = number | string | object | 'NaN'; +interface Raw { + table: Table; + column: number | undefined; + row: number | undefined; + value?: RowValue; +} +export interface Point { + x: RowValue | '_all'; + y: RowValue; + z?: RowValue; + extraMetrics: []; + seriesRaw?: Raw; + xRaw: Raw; + yRaw: Raw; + zRaw?: Raw; + tableRaw?: { + table: Table; + column: number; + row: number; + value: number; + title: string; + }; + parent: Aspect | null; + series?: string; + seriesId?: string; +} +export function getPoint( + table: Table, + x: Aspect, + series: Aspect[] | undefined, + row: Row, + rowIndex: number, + y: Aspect, + z?: Aspect +): Point | undefined { const xRow = x.accessor === -1 ? '_all' : row[x.accessor]; const yRow = row[y.accessor]; const zRow = z && row[z.accessor]; - const point = { + const point: Point = { x: xRow, y: yRow, z: zRow, extraMetrics: [], - yScale: yScale, seriesRaw: series && { table, column: series[0].column, @@ -71,10 +107,9 @@ export function getPoint(table, x, series, yScale, row, rowIndex, y, z) { } if (series) { - const seriesArray = series.length ? series : [series]; - point.series = seriesArray + point.series = series .map(s => { - const fieldFormatter = getFormat(s.format); + const fieldFormatter = getFormatService().deserialize(s.format); return fieldFormatter.convert(row[s.accessor]); }) .join(' - '); @@ -84,9 +119,5 @@ export function getPoint(table, x, series, yScale, row, rowIndex, y, z) { point.series = y.title; } - if (yScale) { - point.y *= yScale; - } - return point; } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts new file mode 100644 index 0000000000000..6b94b9de8e15f --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts @@ -0,0 +1,281 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getSeries } from './_get_series'; +import { setFormatService } from '../../../services'; +import { Chart, Aspect } from './point_series'; +import { Table, Column } from '../../types'; +import { Serie } from './_add_to_siri'; +import { Point } from './_get_point'; + +describe('getSeries', function() { + beforeAll(() => { + setFormatService({ + deserialize: () => ({ + convert: jest.fn(v => v), + }), + } as any); + }); + + it('produces a single series with points for each row', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: '0' }], + y: [{ accessor: '1', title: 'y' }], + z: [{ accessor: '2' }], + }, + } as Chart; + + const series = getSeries(table, chart); + + expect(series).toEqual(expect.any(Array)); + expect(series).toHaveLength(1); + + const siri = series[0]; + + expect(siri).toEqual(expect.any(Object)); + expect(siri).toHaveProperty('label', chart.aspects.y[0].title); + expect(siri).toHaveProperty('values'); + + expect(siri.values).toEqual(expect.any(Array)); + expect(siri.values).toHaveLength(5); + + siri.values.forEach(point => { + expect(point).toHaveProperty('x', 1); + expect(point).toHaveProperty('y', 2); + expect(point).toHaveProperty('z', 3); + }); + }); + + it('adds the seriesId to each point', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: '0' }], + y: [ + { accessor: '1', title: '0' }, + { accessor: '2', title: '1' }, + ], + }, + } as Chart; + + const series = getSeries(table, chart); + + series[0].values.forEach(point => { + expect(point).toHaveProperty('seriesId', '1'); + }); + + series[1].values.forEach(point => { + expect(point).toHaveProperty('seriesId', '2'); + }); + }); + + it('produces multiple series if there are multiple y aspects', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: '0' }], + y: [ + { accessor: '1', title: '0' }, + { accessor: '2', title: '1' }, + ], + }, + } as Chart; + + const series = getSeries(table, chart); + + expect(series).toEqual(expect.any(Array)); + expect(series).toHaveLength(2); + + series.forEach(function(siri: Serie, i: number) { + expect(siri).toEqual(expect.any(Object)); + expect(siri).toHaveProperty('label', '' + i); + expect(siri).toHaveProperty('values'); + + expect(siri.values).toEqual(expect.any(Array)); + expect(siri.values).toHaveLength(5); + + siri.values.forEach(function(point: Point) { + expect(point).toHaveProperty('x', 1); + expect(point).toHaveProperty('y', i + 2); + }); + }); + }); + + it('produces multiple series if there is a series aspect', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: -1 } as Aspect], + series: [{ accessor: '0' }], + y: [{ accessor: '1', title: '0' }], + }, + } as Chart; + + const series = getSeries(table, chart); + + expect(series).toEqual(expect.any(Array)); + expect(series).toHaveLength(2); + + series.forEach(function(siri: Serie, i: number) { + expect(siri).toEqual(expect.any(Object)); + expect(siri).toHaveProperty('label', '' + i); + expect(siri).toHaveProperty('values'); + + expect(siri.values).toEqual(expect.any(Array)); + expect(siri.values).toHaveLength(3); + + siri.values.forEach(function(point: Point) { + expect(point).toHaveProperty('y', 2); + }); + }); + }); + + it('produces multiple series if there is a series aspect and multiple y aspects', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 0, '1': 3, '2': 4 }, + { '0': 1, '1': 3, '2': 4 }, + { '0': 0, '1': 3, '2': 4 }, + { '0': 1, '1': 3, '2': 4 }, + { '0': 0, '1': 3, '2': 4 }, + { '0': 1, '1': 3, '2': 4 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: -1 } as Aspect], + series: [{ accessor: '0' }], + y: [ + { accessor: '1', title: '0' }, + { accessor: '2', title: '1' }, + ], + }, + } as Chart; + + const series = getSeries(table, chart); + + expect(series).toEqual(expect.any(Array)); + expect(series).toHaveLength(4); // two series * two metrics + + checkSiri(series[0], '0: 0', 3); + checkSiri(series[1], '0: 1', 4); + checkSiri(series[2], '1: 0', 3); + checkSiri(series[3], '1: 1', 4); + + function checkSiri(siri: Serie, label: string, y: number) { + expect(siri).toEqual(expect.any(Object)); + expect(siri).toHaveProperty('label', label); + expect(siri).toHaveProperty('values'); + + expect(siri.values).toEqual(expect.any(Array)); + expect(siri.values).toHaveLength(3); + + siri.values.forEach(function(point: Point) { + expect(point).toHaveProperty('y', y); + }); + } + }); + + it('produces a series list in the same order as its corresponding metric column', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 0, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: -1 } as Aspect], + series: [{ accessor: '0' }], + y: [ + { accessor: '1', title: '0' }, + { accessor: '2', title: '1' }, + ], + }, + } as Chart; + + const series = getSeries(table, chart); + expect(series[0]).toHaveProperty('label', '0: 0'); + expect(series[1]).toHaveProperty('label', '0: 1'); + expect(series[2]).toHaveProperty('label', '1: 0'); + expect(series[3]).toHaveProperty('label', '1: 1'); + + // switch the order of the y columns + chart.aspects.y = chart.aspects.y.reverse(); + chart.aspects.y.forEach(function(y: any, i) { + y.i = i; + }); + + const series2 = getSeries(table, chart); + expect(series2[0]).toHaveProperty('label', '0: 1'); + expect(series2[1]).toHaveProperty('label', '0: 0'); + expect(series2[2]).toHaveProperty('label', '1: 1'); + expect(series2[3]).toHaveProperty('label', '1: 0'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts new file mode 100644 index 0000000000000..edde5b69af022 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { partial } from 'lodash'; +import { getPoint } from './_get_point'; +import { addToSiri, Serie } from './_add_to_siri'; +import { Chart } from './point_series'; +import { Table } from '../../types'; + +export function getSeries(table: Table, chart: Chart) { + const aspects = chart.aspects; + const xAspect = aspects.x[0]; + const yAspect = aspects.y[0]; + const zAspect = aspects.z && aspects.z[0]; + const multiY = Array.isArray(aspects.y) && aspects.y.length > 1; + + const partGetPoint = partial(getPoint, table, xAspect, aspects.series); + + const seriesMap = new Map(); + + table.rows.forEach((row, rowIndex) => { + if (!multiY) { + const point = partGetPoint(row, rowIndex, yAspect, zAspect); + if (point) { + const id = `${point.series}-${yAspect.accessor}`; + point.seriesId = id; + addToSiri( + seriesMap, + point, + id, + point.series, + yAspect.format, + zAspect && zAspect.format, + zAspect && zAspect.title + ); + } + return; + } + + aspects.y.forEach(function(y) { + const point = partGetPoint(row, rowIndex, y, zAspect); + if (!point) { + return; + } + + // use the point's y-axis as it's series by default, + // but augment that with series aspect if it's actually + // available + let seriesId = y.accessor; + let seriesLabel = y.title; + + if (aspects.series) { + const prefix = point.series ? point.series + ': ' : ''; + seriesId = prefix + seriesId; + seriesLabel = prefix + seriesLabel; + } + + point.seriesId = seriesId; + addToSiri( + seriesMap, + point, + seriesId as string, + seriesLabel, + y.format, + zAspect && zAspect.format, + zAspect && zAspect.title + ); + }); + }); + + return [...seriesMap.values()]; +} diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts similarity index 51% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts index a8512edee658b..d3049d7675408 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts @@ -17,14 +17,22 @@ * under the License. */ -import expect from '@kbn/expect'; import moment from 'moment'; -import { initXAxis } from '../_init_x_axis'; -import { makeFakeXAspect } from '../_fake_x_aspect'; +import { initXAxis } from './_init_x_axis'; +import { makeFakeXAspect } from './_fake_x_aspect'; +import { + Aspects, + Chart, + DateHistogramOrdered, + DateHistogramParams, + HistogramOrdered, + HistogramParams, +} from './point_series'; +import { Table, Column } from '../../types'; describe('initXAxis', function() { - let chart; - let table; + let chart: Chart; + let table: Table; beforeEach(function() { chart = { @@ -32,50 +40,48 @@ describe('initXAxis', function() { x: [ { ...makeFakeXAspect(), - accessor: 0, + accessor: '0', title: 'label', }, ], - }, - }; + } as Aspects, + } as Chart; table = { - columns: [{ id: '0' }], + columns: [{ id: '0' } as Column], rows: [{ '0': 'hello' }, { '0': 'world' }, { '0': 'foo' }, { '0': 'bar' }, { '0': 'baz' }], }; }); it('sets the xAxisFormatter if the agg is not ordered', function() { initXAxis(chart, table); - expect(chart) - .to.have.property('xAxisLabel', 'label') - .and.have.property('xAxisFormat', chart.aspects.x[0].format); + expect(chart).toHaveProperty('xAxisLabel', 'label'); + expect(chart).toHaveProperty('xAxisFormat', chart.aspects.x[0].format); }); it('makes the chart ordered if the agg is ordered', function() { - chart.aspects.x[0].params.interval = 10; + (chart.aspects.x[0].params as HistogramParams).interval = 10; initXAxis(chart, table); - expect(chart) - .to.have.property('xAxisLabel', 'label') - .and.have.property('xAxisFormat', chart.aspects.x[0].format) - .and.have.property('ordered'); + expect(chart).toHaveProperty('xAxisLabel', 'label'); + expect(chart).toHaveProperty('xAxisFormat', chart.aspects.x[0].format); + expect(chart).toHaveProperty('ordered'); }); describe('xAxisOrderedValues', function() { it('sets the xAxisOrderedValues property', function() { initXAxis(chart, table); - expect(chart).to.have.property('xAxisOrderedValues'); + expect(chart).toHaveProperty('xAxisOrderedValues'); }); it('returns a list of values, preserving the table order', function() { initXAxis(chart, table); - expect(chart.xAxisOrderedValues).to.eql(['hello', 'world', 'foo', 'bar', 'baz']); + expect(chart.xAxisOrderedValues).toEqual(['hello', 'world', 'foo', 'bar', 'baz']); }); it('only returns unique values', function() { table = { - columns: [{ id: '0' }], + columns: [{ id: '0' } as Column], rows: [ { '0': 'hello' }, { '0': 'world' }, @@ -88,45 +94,46 @@ describe('initXAxis', function() { ], }; initXAxis(chart, table); - expect(chart.xAxisOrderedValues).to.eql(['hello', 'world', 'foo', 'bar', 'baz']); + expect(chart.xAxisOrderedValues).toEqual(['hello', 'world', 'foo', 'bar', 'baz']); }); it('returns the defaultValue if using fake x aspect', function() { chart = { aspects: { x: [makeFakeXAspect()], - }, - }; + } as Aspects, + } as Chart; initXAxis(chart, table); - expect(chart.xAxisOrderedValues).to.eql(['_all']); + expect(chart.xAxisOrderedValues).toEqual(['_all']); }); }); it('reads the date interval param from the x agg', function() { - chart.aspects.x[0].params.interval = 'P1D'; - chart.aspects.x[0].params.intervalESValue = 1; - chart.aspects.x[0].params.intervalESUnit = 'd'; - chart.aspects.x[0].params.date = true; + const dateHistogramParams = chart.aspects.x[0].params as DateHistogramParams; + dateHistogramParams.interval = 'P1D'; + dateHistogramParams.intervalESValue = 1; + dateHistogramParams.intervalESUnit = 'd'; + dateHistogramParams.date = true; initXAxis(chart, table); - expect(chart) - .to.have.property('xAxisLabel', 'label') - .and.have.property('xAxisFormat', chart.aspects.x[0].format) - .and.have.property('ordered'); + expect(chart).toHaveProperty('xAxisLabel', 'label'); + expect(chart).toHaveProperty('xAxisFormat', chart.aspects.x[0].format); + expect(chart).toHaveProperty('ordered'); - expect(moment.isDuration(chart.ordered.interval)).to.be(true); - expect(chart.ordered.interval.toISOString()).to.eql('P1D'); - expect(chart.ordered.intervalESValue).to.be(1); - expect(chart.ordered.intervalESUnit).to.be('d'); + expect(chart.ordered).toEqual(expect.any(Object)); + const { intervalESUnit, intervalESValue, interval } = chart.ordered as DateHistogramOrdered; + expect(moment.isDuration(interval)).toBe(true); + expect(interval.toISOString()).toEqual('P1D'); + expect(intervalESValue).toBe(1); + expect(intervalESUnit).toBe('d'); }); it('reads the numeric interval param from the x agg', function() { - chart.aspects.x[0].params.interval = 0.5; + (chart.aspects.x[0].params as HistogramParams).interval = 0.5; initXAxis(chart, table); - expect(chart) - .to.have.property('xAxisLabel', 'label') - .and.have.property('xAxisFormat', chart.aspects.x[0].format) - .and.have.property('ordered'); + expect(chart).toHaveProperty('xAxisLabel', 'label'); + expect(chart).toHaveProperty('xAxisFormat', chart.aspects.x[0].format); + expect(chart).toHaveProperty('ordered'); - expect(chart.ordered.interval).to.eql(0.5); + expect((chart.ordered as HistogramOrdered).interval).toEqual(0.5); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts similarity index 73% rename from src/legacy/ui/public/agg_response/point_series/_init_x_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts index 4a81486783b08..9d16c4857be00 100644 --- a/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts @@ -19,27 +19,31 @@ import { uniq } from 'lodash'; import moment from 'moment'; +import { Chart } from './point_series'; +import { Table } from '../../types'; -export function initXAxis(chart, table) { +export function initXAxis(chart: Chart, table: Table) { const { format, title, params, accessor } = chart.aspects.x[0]; chart.xAxisOrderedValues = - accessor === -1 ? [params.defaultValue] : uniq(table.rows.map(r => r[accessor])); + accessor === -1 && 'defaultValue' in params + ? [params.defaultValue] + : uniq(table.rows.map(r => r[accessor])); chart.xAxisFormat = format; chart.xAxisLabel = title; - const { interval, date } = params; - if (interval) { - if (date) { + if ('interval' in params) { + const { interval } = params; + if ('date' in params) { const { intervalESUnit, intervalESValue } = params; chart.ordered = { interval: moment.duration(interval), - intervalESUnit: intervalESUnit, - intervalESValue: intervalESValue, + intervalESUnit, + intervalESValue, }; } else { chart.ordered = { - interval, + interval: params.interval, }; } } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_y_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts similarity index 81% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_init_y_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts index 78cd5334e6c86..df84d69c9f849 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_y_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts @@ -18,8 +18,8 @@ */ import _ from 'lodash'; -import expect from '@kbn/expect'; -import { initYAxis } from '../_init_y_axis'; +import { initYAxis } from './_init_y_axis'; +import { Chart } from './point_series'; describe('initYAxis', function() { const baseChart = { @@ -34,7 +34,7 @@ describe('initYAxis', function() { }, ], }, - }; + } as Chart; describe('with a single y aspect', function() { const singleYBaseChart = _.cloneDeep(baseChart); @@ -43,13 +43,13 @@ describe('initYAxis', function() { it('sets the yAxisFormatter the the field formats convert fn', function() { const chart = _.cloneDeep(singleYBaseChart); initYAxis(chart); - expect(chart).to.have.property('yAxisFormat'); + expect(chart).toHaveProperty('yAxisFormat'); }); it('sets the yAxisLabel', function() { const chart = _.cloneDeep(singleYBaseChart); initYAxis(chart); - expect(chart).to.have.property('yAxisLabel', 'y1'); + expect(chart).toHaveProperty('yAxisLabel', 'y1'); }); }); @@ -58,16 +58,15 @@ describe('initYAxis', function() { const chart = _.cloneDeep(baseChart); initYAxis(chart); - expect(chart).to.have.property('yAxisFormat'); - expect(chart.yAxisFormat) - .to.be(chart.aspects.y[0].format) - .and.not.be(chart.aspects.y[1].format); + expect(chart).toHaveProperty('yAxisFormat'); + expect(chart.yAxisFormat).toBe(chart.aspects.y[0].format); + expect(chart.yAxisFormat).not.toBe(chart.aspects.y[1].format); }); it('does not set the yAxisLabel, it does not make sense to put multiple labels on the same axis', function() { const chart = _.cloneDeep(baseChart); initYAxis(chart); - expect(chart).to.have.property('yAxisLabel', ''); + expect(chart).toHaveProperty('yAxisLabel', ''); }); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_init_y_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts similarity index 82% rename from src/legacy/ui/public/agg_response/point_series/_init_y_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts index 42f5e79a63172..43ba0557949ac 100644 --- a/src/legacy/ui/public/agg_response/point_series/_init_y_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts @@ -17,7 +17,9 @@ * under the License. */ -export function initYAxis(chart) { +import { Chart } from './point_series'; + +export function initYAxis(chart: Chart) { const y = chart.aspects.y; if (Array.isArray(y)) { @@ -28,12 +30,7 @@ export function initYAxis(chart) { const z = chart.aspects.series; if (z) { - if (Array.isArray(z)) { - chart.zAxisFormat = z[0].format; - chart.zAxisLabel = ''; - } else { - chart.zAxisFormat = z.format; - chart.zAxisLabel = z.title; - } + chart.zAxisFormat = z[0].format; + chart.zAxisLabel = ''; } } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts similarity index 76% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts index 2e08be16278d5..25e466f21c3e7 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts @@ -19,8 +19,8 @@ import moment from 'moment'; import _ from 'lodash'; -import expect from '@kbn/expect'; -import { orderedDateAxis } from '../_ordered_date_axis'; +import { orderedDateAxis } from './_ordered_date_axis'; +import { DateHistogramParams, OrderedChart } from './point_series'; describe('orderedDateAxis', function() { const baseArgs = { @@ -46,7 +46,7 @@ describe('orderedDateAxis', function() { }, ], }, - }, + } as OrderedChart, }; describe('ordered object', function() { @@ -54,24 +54,24 @@ describe('orderedDateAxis', function() { const args = _.cloneDeep(baseArgs); orderedDateAxis(args.chart); - expect(args.chart).to.have.property('ordered'); + expect(args.chart).toHaveProperty('ordered'); - expect(args.chart.ordered).to.have.property('date', true); + expect(args.chart.ordered).toHaveProperty('date', true); }); it('sets the min/max when the buckets are bounded', function() { const args = _.cloneDeep(baseArgs); orderedDateAxis(args.chart); - expect(args.chart.ordered).to.have.property('min'); - expect(args.chart.ordered).to.have.property('max'); + expect(args.chart.ordered).toHaveProperty('min'); + expect(args.chart.ordered).toHaveProperty('max'); }); it('does not set the min/max when the buckets are unbounded', function() { const args = _.cloneDeep(baseArgs); - args.chart.aspects.x[0].params.bounds = null; + (args.chart.aspects.x[0].params as DateHistogramParams).bounds = undefined; orderedDateAxis(args.chart); - expect(args.chart.ordered).to.not.have.property('min'); - expect(args.chart.ordered).to.not.have.property('max'); + expect(args.chart.ordered).not.toHaveProperty('min'); + expect(args.chart.ordered).not.toHaveProperty('max'); }); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_ordered_date_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts similarity index 72% rename from src/legacy/ui/public/agg_response/point_series/_ordered_date_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts index a1dd50dc6c71b..193b10a563563 100644 --- a/src/legacy/ui/public/agg_response/point_series/_ordered_date_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts @@ -17,17 +17,17 @@ * under the License. */ -// import moment from 'moment'; +import { OrderedChart } from './point_series'; -export function orderedDateAxis(chart) { +export function orderedDateAxis(chart: OrderedChart) { const x = chart.aspects.x[0]; - const { bounds } = x.params; + const bounds = 'bounds' in x.params ? x.params.bounds : undefined; chart.ordered.date = true; if (bounds) { - chart.ordered.min = isNaN(bounds.min) ? Date.parse(bounds.min) : bounds.min; - chart.ordered.max = isNaN(bounds.max) ? Date.parse(bounds.max) : bounds.max; + chart.ordered.min = typeof bounds.min === 'string' ? Date.parse(bounds.min) : bounds.min; + chart.ordered.max = typeof bounds.max === 'string' ? Date.parse(bounds.max) : bounds.max; } else { chart.ordered.endzones = false; } diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts similarity index 93% rename from src/legacy/core_plugins/management/public/np_ready/services/index.ts rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts index 9df010223542b..9bfba4de966be 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './index_pattern_management'; +export { buildPointSeriesData } from './point_series'; diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_main.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts similarity index 62% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_main.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts index a4c23cb537488..3725bf06660e2 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_main.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts @@ -18,17 +18,25 @@ */ import _ from 'lodash'; -import expect from '@kbn/expect'; -import { buildPointSeriesData } from '../point_series'; +import { buildPointSeriesData, Dimensions } from './point_series'; +import { Table, Column } from '../../types'; +import { setFormatService } from '../../../services'; +import { Serie } from './_add_to_siri'; describe('pointSeriesChartDataFromTable', function() { - this.slow(1000); + beforeAll(() => { + setFormatService({ + deserialize: () => ({ + convert: jest.fn(v => v), + }), + } as any); + }); it('handles a table with just a count', function() { const table = { - columns: [{ id: '0' }], + columns: [{ id: '0' } as Column], rows: [{ '0': 100 }], - }; + } as Table; const chartData = buildPointSeriesData(table, { y: [ { @@ -36,16 +44,15 @@ describe('pointSeriesChartDataFromTable', function() { params: {}, }, ], - }); + } as Dimensions); - expect(chartData).to.be.an('object'); - expect(chartData.series).to.be.an('array'); - expect(chartData.series).to.have.length(1); + expect(chartData).toEqual(expect.any(Object)); + expect(chartData.series).toEqual(expect.any(Array)); + expect(chartData.series).toHaveLength(1); const series = chartData.series[0]; - expect(series.values).to.have.length(1); - expect(series.values[0]) - .to.have.property('x', '_all') - .and.have.property('y', 100); + expect(series.values).toHaveLength(1); + expect(series.values[0]).toHaveProperty('x', '_all'); + expect(series.values[0]).toHaveProperty('y', 100); }); it('handles a table with x and y column', function() { @@ -59,21 +66,21 @@ describe('pointSeriesChartDataFromTable', function() { { '0': 2, '1': 200 }, { '0': 3, '1': 200 }, ], - }; + } as Table; const dimensions = { - x: [{ accessor: 0, params: {} }], + x: { accessor: 0, params: {} }, y: [{ accessor: 1, params: {} }], - }; + } as Dimensions; const chartData = buildPointSeriesData(table, dimensions); - expect(chartData).to.be.an('object'); - expect(chartData.series).to.be.an('array'); - expect(chartData.series).to.have.length(1); + expect(chartData).toEqual(expect.any(Object)); + expect(chartData.series).toEqual(expect.any(Array)); + expect(chartData.series).toHaveLength(1); const series = chartData.series[0]; - expect(series).to.have.property('label', 'Count'); - expect(series.values).to.have.length(3); + expect(series).toHaveProperty('label', 'Count'); + expect(series.values).toHaveLength(3); }); it('handles a table with an x and two y aspects', function() { @@ -84,23 +91,23 @@ describe('pointSeriesChartDataFromTable', function() { { '0': 2, '1': 200, '2': 300 }, { '0': 3, '1': 200, '2': 300 }, ], - }; + } as Table; const dimensions = { - x: [{ accessor: 0, params: {} }], + x: { accessor: 0, params: {} }, y: [ { accessor: 1, params: {} }, { accessor: 2, params: {} }, ], - }; + } as Dimensions; const chartData = buildPointSeriesData(table, dimensions); - expect(chartData).to.be.an('object'); - expect(chartData.series).to.be.an('array'); - expect(chartData.series).to.have.length(2); - chartData.series.forEach(function(siri, i) { - expect(siri).to.have.property('label', `Count-${i}`); - expect(siri.values).to.have.length(3); + expect(chartData).toEqual(expect.any(Object)); + expect(chartData.series).toEqual(expect.any(Array)); + expect(chartData.series).toHaveLength(2); + chartData.series.forEach(function(siri: Serie, i: number) { + expect(siri).toHaveProperty('label', `Count-${i}`); + expect(siri.values).toHaveLength(3); }); }); @@ -121,21 +128,21 @@ describe('pointSeriesChartDataFromTable', function() { }; const dimensions = { - x: [{ accessor: 0, params: {} }], + x: { accessor: 0, params: {} }, series: [{ accessor: 1, params: {} }], y: [ { accessor: 2, params: {} }, { accessor: 3, params: {} }, ], - }; + } as Dimensions; const chartData = buildPointSeriesData(table, dimensions); - expect(chartData).to.be.an('object'); - expect(chartData.series).to.be.an('array'); + expect(chartData).toEqual(expect.any(Object)); + expect(chartData.series).toEqual(expect.any(Array)); // one series for each extension, and then one for each metric inside - expect(chartData.series).to.have.length(4); - chartData.series.forEach(function(siri) { - expect(siri.values).to.have.length(2); + expect(chartData.series).toHaveLength(4); + chartData.series.forEach(function(siri: Serie) { + expect(siri.values).toHaveLength(2); }); }); }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts new file mode 100644 index 0000000000000..a1681e0d71bd3 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Duration } from 'moment'; +import { getSeries } from './_get_series'; +import { getAspects } from './_get_aspects'; +import { initYAxis } from './_init_y_axis'; +import { initXAxis } from './_init_x_axis'; +import { orderedDateAxis } from './_ordered_date_axis'; +import { Serie } from './_add_to_siri'; +import { Column, Table } from '../../types'; + +export interface DateHistogramParams { + date: boolean; + interval: string; + intervalESValue: number; + intervalESUnit: string; + format: string; + bounds?: { + min: string | number; + max: string | number; + }; +} +export interface HistogramParams { + interval: number; +} +export interface FakeParams { + defaultValue: string; +} +export interface Dimension { + accessor: number; + format: { + id?: string; + params?: { pattern?: string; [key: string]: any }; + }; + params: DateHistogramParams | HistogramParams | FakeParams | {}; +} + +export interface Dimensions { + x: Dimension | null; + y: Dimension[]; + z?: Dimension[]; + series?: Dimension | Dimension[]; +} +export interface Aspect { + accessor: Column['id']; + column?: Dimension['accessor']; + title: Column['name']; + format: Dimension['format']; + params: Dimension['params']; +} +export type Aspects = { x: Aspect[]; y: Aspect[] } & { [key in keyof Dimensions]?: Aspect[] }; + +export interface DateHistogramOrdered { + interval: Duration; + intervalESUnit: DateHistogramParams['intervalESUnit']; + intervalESValue: DateHistogramParams['intervalESValue']; +} +export interface HistogramOrdered { + interval: HistogramParams['interval']; +} + +type Ordered = (DateHistogramOrdered | HistogramOrdered) & { + date?: boolean; + min?: number; + max?: number; + endzones?: boolean; +}; + +export interface Chart { + aspects: Aspects; + series: Serie[]; + xAxisOrderedValues?: Array; + xAxisFormat?: Dimension['format']; + xAxisLabel?: Column['name']; + yAxisFormat?: Dimension['format']; + yAxisLabel?: Column['name']; + zAxisFormat?: Dimension['format']; + zAxisLabel?: Column['name']; + ordered?: Ordered; +} + +export type OrderedChart = Chart & { ordered: Ordered }; + +export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { + const chart = { + aspects: getAspects(table, dimensions), + } as Chart; + + initXAxis(chart, table); + initYAxis(chart); + + if ('date' in chart.aspects.x[0].params) { + // initXAxis will turn `chart` into an `OrderedChart if it is a date axis` + orderedDateAxis(chart as OrderedChart); + } + + chart.series = getSeries(table, chart); + + delete chart.aspects; + return chart; +}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js index 9ba86c5181a4c..b5f80303b1d74 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js @@ -17,8 +17,8 @@ * under the License. */ -import { buildHierarchicalData, buildPointSeriesData } from '../legacy_imports'; import { getFormatService } from '../services'; +import { buildHierarchicalData, buildPointSeriesData } from './helpers'; function tableResponseHandler(table, dimensions) { const converted = { tables: [] }; @@ -72,7 +72,7 @@ function tableResponseHandler(table, dimensions) { function convertTableGroup(tableGroup, convertTable) { const tables = tableGroup.tables; - if (!tables.length) return; + if (!tables || !tables.length) return; const firstChild = tables[0]; if (firstChild.columns) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts new file mode 100644 index 0000000000000..4a8bebc493235 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { setFormatService } from '../services'; + +jest.mock('./helpers', () => ({ + buildHierarchicalData: jest.fn(() => ({})), + buildPointSeriesData: jest.fn(() => ({})), +})); + +// @ts-ignore +import { vislibSeriesResponseHandler, vislibSlicesResponseHandler } from './response_handler'; +import { buildHierarchicalData, buildPointSeriesData } from './helpers'; +import { Table } from './types'; + +describe('response_handler', () => { + describe('vislibSlicesResponseHandler', () => { + test('should not call buildHierarchicalData when no columns', () => { + vislibSlicesResponseHandler({ rows: [] }, {}); + expect(buildHierarchicalData).not.toHaveBeenCalled(); + }); + + test('should call buildHierarchicalData', () => { + const response = { + rows: [{ 'col-0-1': 1 }], + columns: [{ id: 'col-0-1', name: 'Count' }], + }; + const dimensions = { metric: { accessor: 0 } }; + vislibSlicesResponseHandler(response, dimensions); + + expect(buildHierarchicalData).toHaveBeenCalledWith( + { columns: [...response.columns], rows: [...response.rows] }, + dimensions + ); + }); + }); + + describe('vislibSeriesResponseHandler', () => { + let resp: Table; + let expected: any; + + beforeAll(() => { + setFormatService({ + deserialize: () => ({ + convert: jest.fn(v => v), + }), + } as any); + }); + + beforeAll(() => { + resp = { + rows: [ + { 'col-0-3': 158599872, 'col-1-1': 1 }, + { 'col-0-3': 158599893, 'col-1-1': 2 }, + { 'col-0-3': 158599908, 'col-1-1': 1 }, + ], + columns: [ + { id: 'col-0-3', name: 'timestamp per 30 seconds' }, + { id: 'col-1-1', name: 'Count' }, + ], + } as Table; + + const colId = resp.columns[0].id; + expected = [ + { label: `${resp.rows[0][colId]}: ${resp.columns[0].name}` }, + { label: `${resp.rows[1][colId]}: ${resp.columns[0].name}` }, + { label: `${resp.rows[2][colId]}: ${resp.columns[0].name}` }, + ]; + }); + + test('should not call buildPointSeriesData when no columns', () => { + vislibSeriesResponseHandler({ rows: [] }, {}); + expect(buildPointSeriesData).not.toHaveBeenCalled(); + }); + + test('should call buildPointSeriesData', () => { + const response = { + rows: [{ 'col-0-1': 1 }], + columns: [{ id: 'col-0-1', name: 'Count' }], + }; + const dimensions = { x: null, y: { accessor: 0 } }; + vislibSeriesResponseHandler(response, dimensions); + + expect(buildPointSeriesData).toHaveBeenCalledWith( + { columns: [...response.columns], rows: [...response.rows] }, + dimensions + ); + }); + + test('should split columns', () => { + const dimensions = { + x: null, + y: [{ accessor: 1 }], + splitColumn: [{ accessor: 0 }], + }; + + const convertedResp = vislibSlicesResponseHandler(resp, dimensions); + expect(convertedResp.columns).toHaveLength(resp.rows.length); + expect(convertedResp.columns).toEqual(expected); + }); + + test('should split rows', () => { + const dimensions = { + x: null, + y: [{ accessor: 1 }], + splitRow: [{ accessor: 0 }], + }; + + const convertedResp = vislibSlicesResponseHandler(resp, dimensions); + expect(convertedResp.rows).toHaveLength(resp.rows.length); + expect(convertedResp.rows).toEqual(expected); + }); + }); +}); diff --git a/src/legacy/ui/public/agg_response/point_series/_add_to_siri.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts similarity index 67% rename from src/legacy/ui/public/agg_response/point_series/_add_to_siri.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts index 9a0fcbc7b267c..ad59603663b84 100644 --- a/src/legacy/ui/public/agg_response/point_series/_add_to_siri.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts @@ -17,22 +17,26 @@ * under the License. */ -export function addToSiri(series, point, id, yLabel, yFormat, zFormat, zLabel) { - id = id == null ? '' : id + ''; +export interface Column { + // -1 value can be in a fake X aspect + id: string | -1; + name: string; +} - if (series.has(id)) { - series.get(id).values.push(point); - return; - } +export interface Row { + [key: string]: number | string | object; +} - series.set(id, { - id: id.split('-').pop(), - rawId: id, - label: yLabel == null ? id : yLabel, - count: 0, - values: [point], - format: yFormat, - zLabel, - zFormat, - }); +export interface TableParent { + table: Table; + tables?: Table[]; + column: number; + row: number; + key: number; + name: string; +} +export interface Table { + columns: Column[]; + rows: Row[]; + $parent?: TableParent; } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_add_to_siri.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_add_to_siri.js deleted file mode 100644 index 43a10ebbfb12e..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_add_to_siri.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { addToSiri } from '../_add_to_siri'; - -describe('addToSiri', function() { - it('creates a new series the first time it sees an id', function() { - const series = new Map(); - const point = {}; - const id = 'id'; - addToSiri(series, point, id, id, { id: id }); - - expect(series.has(id)).to.be(true); - expect(series.get(id)).to.be.an('object'); - expect(series.get(id).label).to.be(id); - expect(series.get(id).values).to.have.length(1); - expect(series.get(id).values[0]).to.be(point); - }); - - it('adds points to existing series if id has been seen', function() { - const series = new Map(); - const id = 'id'; - - const point = {}; - addToSiri(series, point, id, id, { id: id }); - - const point2 = {}; - addToSiri(series, point2, id, id, { id: id }); - - expect(series.has(id)).to.be(true); - expect(series.get(id)).to.be.an('object'); - expect(series.get(id).label).to.be(id); - expect(series.get(id).values).to.have.length(2); - expect(series.get(id).values[0]).to.be(point); - expect(series.get(id).values[1]).to.be(point2); - }); - - it('allows overriding the series label', function() { - const series = new Map(); - const id = 'id'; - const label = 'label'; - const point = {}; - addToSiri(series, point, id, label, { id: id }); - - expect(series.has(id)).to.be(true); - expect(series.get(id)).to.be.an('object'); - expect(series.get(id).label).to.be(label); - expect(series.get(id).values).to.have.length(1); - expect(series.get(id).values[0]).to.be(point); - }); - - it('correctly sets id and rawId', function() { - const series = new Map(); - const id = 'id-id2'; - - const point = {}; - addToSiri(series, point, id); - - expect(series.has(id)).to.be(true); - expect(series.get(id)).to.be.an('object'); - expect(series.get(id).label).to.be(id); - expect(series.get(id).rawId).to.be(id); - expect(series.get(id).id).to.be('id2'); - }); -}); diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_point.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_get_point.js deleted file mode 100644 index 0eb2c608d6d6c..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_point.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { getPoint } from '../_get_point'; - -describe('getPoint', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 1, '1': 2, '2': 3 }, - { '0': 4, '1': 'NaN', '2': 6 }, - ], - }; - - describe('Without series aspect', function() { - let seriesAspect; - let xAspect; - let yAspect; - let yScale; - - beforeEach(function() { - seriesAspect = null; - xAspect = { accessor: 0 }; - yAspect = { accessor: 1, title: 'Y' }; - yScale = 5; - }); - - it('properly unwraps and scales values', function() { - const row = table.rows[0]; - const zAspect = { accessor: 2 }; - const point = getPoint(table, xAspect, seriesAspect, yScale, row, 0, yAspect, zAspect); - - expect(point) - .to.have.property('x', 1) - .and.have.property('y', 10) - .and.have.property('z', 3) - .and.have.property('series', yAspect.title); - }); - - it('ignores points with a y value of NaN', function() { - const row = table.rows[1]; - const point = getPoint(table, xAspect, seriesAspect, yScale, row, 1, yAspect); - expect(point).to.be(void 0); - }); - }); - - describe('With series aspect', function() { - let row; - let xAspect; - let yAspect; - let yScale; - - beforeEach(function() { - row = table.rows[0]; - xAspect = { accessor: 0 }; - yAspect = { accessor: 2 }; - yScale = null; - }); - - it('properly unwraps and scales values', function() { - const seriesAspect = [{ accessor: 1 }]; - const point = getPoint(table, xAspect, seriesAspect, yScale, row, 0, yAspect); - - expect(point) - .to.have.property('x', 1) - .and.have.property('series', '2') - .and.have.property('y', 3); - }); - - it('properly formats series values', function() { - const seriesAspect = [{ accessor: 1, format: { id: 'number', params: { pattern: '$' } } }]; - const point = getPoint(table, xAspect, seriesAspect, yScale, row, 0, yAspect); - - expect(point) - .to.have.property('x', 1) - .and.have.property('series', '$2') - .and.have.property('y', 3); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_series.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_get_series.js deleted file mode 100644 index 1727994976383..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_series.js +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import expect from '@kbn/expect'; -import { getSeries } from '../_get_series'; - -describe('getSeries', function() { - it('produces a single series with points for each row', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: 0 }], - y: [{ accessor: 1, title: 'y' }], - z: { accessor: 2 }, - }, - }; - - const series = getSeries(table, chart); - - expect(series) - .to.be.an('array') - .and.to.have.length(1); - - const siri = series[0]; - expect(siri) - .to.be.an('object') - .and.have.property('label', chart.aspects.y.title) - .and.have.property('values'); - - expect(siri.values) - .to.be.an('array') - .and.have.length(5); - - siri.values.forEach(function(point) { - expect(point) - .to.have.property('x', 1) - .and.property('y', 2) - .and.property('z', 3); - }); - }); - - it('adds the seriesId to each point', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: 0 }], - y: [ - { accessor: 1, title: '0' }, - { accessor: 2, title: '1' }, - ], - }, - }; - - const series = getSeries(table, chart); - - series[0].values.forEach(function(point) { - expect(point).to.have.property('seriesId', 1); - }); - - series[1].values.forEach(function(point) { - expect(point).to.have.property('seriesId', 2); - }); - }); - - it('produces multiple series if there are multiple y aspects', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: 0 }], - y: [ - { accessor: 1, title: '0' }, - { accessor: 2, title: '1' }, - ], - }, - }; - - const series = getSeries(table, chart); - - expect(series) - .to.be.an('array') - .and.to.have.length(2); - - series.forEach(function(siri, i) { - expect(siri) - .to.be.an('object') - .and.have.property('label', '' + i) - .and.have.property('values'); - - expect(siri.values) - .to.be.an('array') - .and.have.length(5); - - siri.values.forEach(function(point) { - expect(point) - .to.have.property('x', 1) - .and.property('y', i + 2); - }); - }); - }); - - it('produces multiple series if there is a series aspect', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: -1 }], - series: [{ accessor: 0, fieldFormatter: _.identity }], - y: [{ accessor: 1, title: '0' }], - }, - }; - - const series = getSeries(table, chart); - - expect(series) - .to.be.an('array') - .and.to.have.length(2); - - series.forEach(function(siri, i) { - expect(siri) - .to.be.an('object') - .and.have.property('label', '' + i) - .and.have.property('values'); - - expect(siri.values) - .to.be.an('array') - .and.have.length(3); - - siri.values.forEach(function(point) { - expect(point).to.have.property('y', 2); - }); - }); - }); - - it('produces multiple series if there is a series aspect and multiple y aspects', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 0, '1': 3, '2': 4 }, - { '0': 1, '1': 3, '2': 4 }, - { '0': 0, '1': 3, '2': 4 }, - { '0': 1, '1': 3, '2': 4 }, - { '0': 0, '1': 3, '2': 4 }, - { '0': 1, '1': 3, '2': 4 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: -1 }], - series: [{ accessor: 0, fieldFormatter: _.identity }], - y: [ - { accessor: 1, title: '0' }, - { accessor: 2, title: '1' }, - ], - }, - }; - - const series = getSeries(table, chart); - - expect(series) - .to.be.an('array') - .and.to.have.length(4); // two series * two metrics - - checkSiri(series[0], '0: 0', 3); - checkSiri(series[1], '0: 1', 4); - checkSiri(series[2], '1: 0', 3); - checkSiri(series[3], '1: 1', 4); - - function checkSiri(siri, label, y) { - expect(siri) - .to.be.an('object') - .and.have.property('label', label) - .and.have.property('values'); - - expect(siri.values) - .to.be.an('array') - .and.have.length(3); - - siri.values.forEach(function(point) { - expect(point).to.have.property('y', y); - }); - } - }); - - it('produces a series list in the same order as its corresponding metric column', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 0, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: -1 }], - series: [{ accessor: 0, fieldFormatter: _.identity }], - y: [ - { accessor: 1, title: '0' }, - { accessor: 2, title: '1' }, - ], - }, - }; - - const series = getSeries(table, chart); - expect(series[0]).to.have.property('label', '0: 0'); - expect(series[1]).to.have.property('label', '0: 1'); - expect(series[2]).to.have.property('label', '1: 0'); - expect(series[3]).to.have.property('label', '1: 1'); - - // switch the order of the y columns - chart.aspects.y = chart.aspects.y.reverse(); - chart.aspects.y.forEach(function(y, i) { - y.i = i; - }); - - const series2 = getSeries(table, chart); - expect(series2[0]).to.have.property('label', '0: 1'); - expect(series2[1]).to.have.property('label', '0: 0'); - expect(series2[2]).to.have.property('label', '1: 1'); - expect(series2[3]).to.have.property('label', '1: 0'); - }); -}); diff --git a/src/legacy/ui/public/agg_response/point_series/_get_series.js b/src/legacy/ui/public/agg_response/point_series/_get_series.js deleted file mode 100644 index 73c1735191abc..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/_get_series.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { getPoint } from './_get_point'; -import { addToSiri } from './_add_to_siri'; - -export function getSeries(table, chart) { - const aspects = chart.aspects; - const xAspect = aspects.x[0]; - const yAspect = aspects.y[0]; - const zAspect = aspects.z && aspects.z.length ? aspects.z[0] : aspects.z; - const multiY = Array.isArray(aspects.y) && aspects.y.length > 1; - const yScale = chart.yScale; - - const partGetPoint = _.partial(getPoint, table, xAspect, aspects.series, yScale); - - let series = _(table.rows) - .transform(function(series, row, rowIndex) { - if (!multiY) { - const point = partGetPoint(row, rowIndex, yAspect, zAspect); - if (point) { - const id = `${point.series}-${yAspect.accessor}`; - point.seriesId = id; - addToSiri( - series, - point, - id, - point.series, - yAspect.format, - zAspect && zAspect.format, - zAspect && zAspect.title - ); - } - return; - } - - aspects.y.forEach(function(y) { - const point = partGetPoint(row, rowIndex, y, zAspect); - if (!point) return; - - // use the point's y-axis as it's series by default, - // but augment that with series aspect if it's actually - // available - let seriesId = y.accessor; - let seriesLabel = y.title; - - if (aspects.series) { - const prefix = point.series ? point.series + ': ' : ''; - seriesId = prefix + seriesId; - seriesLabel = prefix + seriesLabel; - } - - point.seriesId = seriesId; - addToSiri( - series, - point, - seriesId, - seriesLabel, - y.format, - zAspect && zAspect.format, - zAspect && zAspect.title - ); - }); - }, new Map()) - .thru(series => [...series.values()]) - .value(); - - if (multiY) { - series = _.sortBy(series, function(siri) { - const firstVal = siri.values[0]; - let y; - - if (firstVal) { - y = _.find(aspects.y, function(y) { - return y.accessor === firstVal.accessor; - }); - } - - return y ? y.i : series.length; - }); - } - return series; -} diff --git a/src/legacy/ui/public/agg_response/point_series/point_series.js b/src/legacy/ui/public/agg_response/point_series/point_series.js deleted file mode 100644 index 8489f7bc2ca45..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/point_series.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { getSeries } from './_get_series'; -import { getAspects } from './_get_aspects'; -import { initYAxis } from './_init_y_axis'; -import { initXAxis } from './_init_x_axis'; -import { orderedDateAxis } from './_ordered_date_axis'; - -export const buildPointSeriesData = (table, dimensions) => { - const chart = { - aspects: getAspects(table, dimensions), - }; - - initXAxis(chart, table); - initYAxis(chart); - - if (chart.aspects.x[0].params.date) { - orderedDateAxis(chart); - } - - chart.series = getSeries(table, chart); - - delete chart.aspects; - return chart; -}; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f70ef069dd134..0779d6472671c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -290,6 +290,10 @@ export const npSetup = { }), }, }, + indexPatternManagement: { + list: { addListConfig: sinon.fake() }, + creation: { addCreationConfig: sinon.fake() }, + }, discover: { docViews: { addDocView: sinon.fake(), @@ -325,6 +329,17 @@ export const npStart = { }), }, }, + indexPatternManagement: { + list: { + getType: sinon.fake(), + getIndexPatternCreationOptions: sinon.fake(), + }, + creation: { + getIndexPatternTags: sinon.fake(), + getFieldInfo: sinon.fake(), + areScriptedFieldsEnabled: sinon.fake(), + }, + }, embeddable: { getEmbeddableFactory: sinon.fake(), getEmbeddableFactories: sinon.fake(), diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index b4b5099081759..cdd7e1a994912 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -47,6 +47,10 @@ import { AdvancedSettingsStart, } from '../../../../plugins/advanced_settings/public'; import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public'; +import { + IndexPatternManagementSetup, + IndexPatternManagementStart, +} from '../../../../plugins/index_pattern_management/public'; import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public'; @@ -86,6 +90,7 @@ export interface PluginsSetup { visualizations: VisualizationsSetup; telemetry?: TelemetryPluginSetup; savedObjectsManagement: SavedObjectsManagementPluginSetup; + indexPatternManagement: IndexPatternManagementSetup; } export interface PluginsStart { @@ -107,6 +112,7 @@ export interface PluginsStart { telemetry?: TelemetryPluginStart; dashboard: DashboardStart; savedObjectsManagement: SavedObjectsManagementPluginStart; + indexPatternManagement: IndexPatternManagementStart; } export const npSetup = { diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts index 8cf015d5dff5c..400f31e73ffa1 100644 --- a/src/legacy/ui/public/new_platform/set_services.ts +++ b/src/legacy/ui/public/new_platform/set_services.ts @@ -72,9 +72,11 @@ export function setStartServices(npStart: NpStart) { visualizationsServices.setAggs(npStart.plugins.data.search.aggs); visualizationsServices.setOverlays(npStart.core.overlays); visualizationsServices.setChrome(npStart.core.chrome); + visualizationsServices.setSearch(npStart.plugins.data.search); const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: npStart.core.savedObjects.client, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, visualizationTypes: visualizationsServices.getTypes(), diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index ad4aa97d8ea7a..1093153edbbf7 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -30,33 +30,27 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { function loadStyleSheet(url, cb) { var dom = document.createElement('link'); + dom.rel = 'stylesheet'; + dom.type = 'text/css'; + dom.href = url; dom.addEventListener('error', failure); - dom.setAttribute('rel', 'stylesheet'); - dom.setAttribute('type', 'text/css'); - dom.setAttribute('href', url); dom.addEventListener('load', cb); document.head.appendChild(dom); } function loadScript(url, cb) { var dom = document.createElement('script'); - dom.setAttribute('async', ''); + {{!-- NOTE: async = false is used to trigger async-download/ordered-execution as outlined here: https://www.html5rocks.com/en/tutorials/speed/script-loading/ --}} + dom.async = false; + dom.src = url; dom.addEventListener('error', failure); - dom.setAttribute('src', url); dom.addEventListener('load', cb); document.head.appendChild(dom); } - function load(urlSet, cb) { - if (urlSet.deps) { - load({ urls: urlSet.deps }, function () { - load({ urls: urlSet.urls }, cb); - }); - return; - } - - var pending = urlSet.urls.length; - urlSet.urls.forEach(function (url) { + function load(urls, cb) { + var pending = urls.length; + urls.forEach(function (url) { var innerCb = function () { pending = pending - 1; if (pending === 0 && typeof cb === 'function') { @@ -74,36 +68,27 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { }); } - load({ - deps: [ + load([ {{#each sharedJsDepFilenames}} '{{../regularBundlePath}}/kbn-ui-shared-deps/{{this}}', {{/each}} - ], - urls: [ - { - deps: [ - '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedJsFilename}}', - { - deps: [ - '{{dllBundlePath}}/vendors_runtime.bundle.dll.js' - ], - urls: [ - {{#each dllJsChunks}} - '{{this}}', - {{/each}} - ] - }, - '{{regularBundlePath}}/commons.bundle.js', - ], - urls: [ - '{{regularBundlePath}}/{{appId}}.bundle.js', - {{#each styleSheetPaths}} - '{{this}}', - {{/each}} - ] - } - ] + '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedJsFilename}}', + '{{dllBundlePath}}/vendors_runtime.bundle.dll.js', + {{#each dllJsChunks}} + '{{this}}', + {{/each}} + '{{regularBundlePath}}/commons.bundle.js', + {{!-- '{{regularBundlePath}}/plugin/data/data.plugin.js', --}} + '{{regularBundlePath}}/plugin/kibanaUtils/kibanaUtils.plugin.js', + '{{regularBundlePath}}/plugin/esUiShared/esUiShared.plugin.js', + '{{regularBundlePath}}/plugin/kibanaReact/kibanaReact.plugin.js' + ], function () { + load([ + '{{regularBundlePath}}/{{appId}}.bundle.js', + {{#each styleSheetPaths}} + '{{this}}', + {{/each}} + ]) }); }; } diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index c98fa612dc7af..322d734d9f39f 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -284,7 +284,7 @@ export class DashboardPlugin const { notifications } = core; const { uiActions, - data: { indexPatterns }, + data: { indexPatterns, search }, } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -300,6 +300,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns, + search, chrome: core.chrome, overlays: core.overlays, }); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 2a1e64fa88a02..09357072a13a6 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -18,13 +18,14 @@ */ import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public'; -import { IndexPatternsContract } from '../../../../plugins/data/public'; +import { DataPublicPluginStart, IndexPatternsContract } from '../../../../plugins/data/public'; import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; interface Services { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; + search: DataPublicPluginStart['search']; chrome: ChromeStart; overlays: OverlayStart; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 9ed5ce338ef11..199ef2f9ce950 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -367,6 +367,7 @@ export { SearchStrategyProvider, ISearchSource, SearchSource, + createSearchSource, SearchSourceFields, EsQuerySortValue, SortDirection, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 15067077afc43..2ebe377b3b32f 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -155,7 +155,7 @@ export class DataPublicPlugin implements Plugin({ timefilter: { timefil // @public (undocumented) export const createSavedQueryService: (savedObjectsClient: Pick) => SavedQueryService; +// @public +export const createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise; + // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1668,6 +1672,10 @@ export class SearchSource { // (undocumented) history: SearchRequest[]; onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise): void; + serialize(): { + searchSourceJSON: string; + references: SavedObjectReference[]; + }; // (undocumented) setField(field: K, value: SearchSourceFields[K]): this; // (undocumented) @@ -1882,21 +1890,21 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index e6fd259fabc92..57f3aa85ad944 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -44,20 +44,15 @@ const updateTimeBuckets = ( timefilter: TimefilterContract, customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { - const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; + const bounds = + agg.params.timeRange && agg.fieldIsTimeField() + ? timefilter.calculateBounds(agg.params.timeRange) + : undefined; const buckets = customBuckets || agg.buckets; - buckets.setBounds(agg.fieldIsTimeField() && bounds); + buckets.setBounds(bounds); buckets.setInterval(agg.params.interval); }; -// TODO: Need to incorporate these properly into TimeBuckets -interface ITimeBuckets { - setBounds: Function; - getScaledDateFormat: TimeBuckets['getScaledDateFormat']; - setInterval: Function; - getInterval: Function; -} - export interface DateHistogramBucketAggDependencies { uiSettings: IUiSettingsClient; query: QuerySetup; @@ -65,7 +60,7 @@ export interface DateHistogramBucketAggDependencies { } export interface IBucketDateHistogramAggConfig extends IBucketAggConfig { - buckets: ITimeBuckets; + buckets: TimeBuckets; } export function isDateHistogramBucketAggConfig(agg: any): agg is IBucketDateHistogramAggConfig { @@ -113,7 +108,12 @@ export const getDateHistogramBucketAgg = ({ if (buckets) return buckets; const { timefilter } = query.timefilter; - buckets = new TimeBuckets({ uiSettings }); + buckets = new TimeBuckets({ + 'histogram:maxBars': uiSettings.get('histogram:maxBars'), + 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); updateTimeBuckets(this, timefilter, buckets); return buckets; @@ -206,7 +206,8 @@ export const getDateHistogramBucketAgg = ({ ...dateHistogramInterval(interval.expression), }; - const scaleMetrics = scaleMetricValues && interval.scaled && interval.scale < 1; + const scaleMetrics = + scaleMetricValues && interval.scaled && interval.scale && interval.scale < 1; if (scaleMetrics && aggs) { const metrics = aggs.aggs.filter(a => isMetricAggType(a.type)); const all = every(metrics, (a: IBucketAggConfig) => { @@ -218,7 +219,7 @@ export const getDateHistogramBucketAgg = ({ }); if (all) { output.metricScale = interval.scale; - output.metricScaleText = interval.preScaled.description; + output.metricScaleText = interval.preScaled?.description || ''; } } }, diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts new file mode 100644 index 0000000000000..af3c15167295c --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import moment from 'moment'; + +import { TimeBuckets, TimeBucketsConfig } from './time_buckets'; + +describe('TimeBuckets', () => { + const timeBucketConfig: TimeBucketsConfig = { + 'histogram:maxBars': 4, + 'histogram:barTarget': 3, + dateFormat: 'YYYY-MM-DD', + 'dateFormat:scaled': [ + ['', 'HH:mm:ss.SSS'], + ['PT1S', 'HH:mm:ss'], + ['PT1M', 'HH:mm'], + ['PT1H', 'YYYY-MM-DD HH:mm'], + ['P1DT', 'YYYY-MM-DD'], + ['P1YT', 'YYYY'], + ], + }; + + test('setBounds/getBounds - bounds is correct', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + const timeBucketsBounds = timeBuckets.getBounds(); + + expect(timeBucketsBounds).toEqual(bounds); + }); + + test('setBounds/getBounds - bounds is undefined', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + let timeBucketsBounds = timeBuckets.getBounds(); + + expect(timeBucketsBounds).toEqual(bounds); + + timeBuckets.setBounds(); + timeBucketsBounds = timeBuckets.getBounds(); + + expect(timeBucketsBounds).toBeUndefined(); + }); + + test('setInterval/getInterval - intreval is a string', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + timeBuckets.setInterval('20m'); + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('20 minutes'); + expect(interval.esValue).toEqual(20); + expect(interval.esUnit).toEqual('m'); + expect(interval.expression).toEqual('20m'); + }); + + test('setInterval/getInterval - intreval is a string and bounds is defined', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('20m'); + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('day'); + expect(interval.esValue).toEqual(1); + expect(interval.esUnit).toEqual('d'); + expect(interval.expression).toEqual('1d'); + expect(interval.scaled).toBeTruthy(); + expect(interval.scale).toEqual(0.013888888888888888); + + if (interval.preScaled) { + expect(interval.preScaled.description).toEqual('20 minutes'); + expect(interval.preScaled.esValue).toEqual(20); + expect(interval.preScaled.esUnit).toEqual('m'); + expect(interval.preScaled.expression).toEqual('20m'); + } + }); + + test('setInterval/getInterval - intreval is a "auto"', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + timeBuckets.setInterval('auto'); + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('0 milliseconds'); + expect(interval.esValue).toEqual(0); + expect(interval.esUnit).toEqual('ms'); + expect(interval.expression).toEqual('0ms'); + }); + + test('getScaledDateFormat', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + timeBuckets.setInterval('20m'); + timeBuckets.getScaledDateFormat(); + const format = timeBuckets.getScaledDateFormat(); + expect(format).toEqual('HH:mm'); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts index c14f02e7decdf..b8d6586652d6b 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -17,11 +17,11 @@ * under the License. */ -import _ from 'lodash'; -import moment from 'moment'; +import { isString, isObject as isObjectLodash, isPlainObject, sortBy } from 'lodash'; +import moment, { Moment } from 'moment'; -import { IUiSettingsClient } from 'src/core/public'; import { parseInterval } from '../../../../../../common'; +import { TimeRangeBounds } from '../../../../../query'; import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; import { convertDurationToNormalizedEsInterval, @@ -29,37 +29,30 @@ import { EsInterval, } from './calc_es_interval'; -interface Bounds { - min: Date | number | null; - max: Date | number | null; -} - interface TimeBucketsInterval extends moment.Duration { // TODO double-check whether all of these are needed description: string; esValue: EsInterval['value']; esUnit: EsInterval['unit']; expression: EsInterval['expression']; - overflow: moment.Duration | boolean; - preScaled?: moment.Duration; + preScaled?: TimeBucketsInterval; scale?: number; scaled?: boolean; } function isObject(o: any): o is Record { - return _.isObject(o); -} - -function isString(s: any): s is string { - return _.isString(s); + return isObjectLodash(o); } function isValidMoment(m: any): boolean { return m && 'isValid' in m && m.isValid(); } -interface TimeBucketsConfig { - uiSettings: IUiSettingsClient; +export interface TimeBucketsConfig { + 'histogram:maxBars': number; + 'histogram:barTarget': number; + dateFormat: string; + 'dateFormat:scaled': string[][]; } /** @@ -70,108 +63,17 @@ interface TimeBucketsConfig { * @param {[type]} display [description] */ export class TimeBuckets { - private getConfig: (key: string) => any; - - private _lb: Bounds['min'] = null; - private _ub: Bounds['max'] = null; + private _timeBucketConfig: TimeBucketsConfig; + private _lb: TimeRangeBounds['min']; + private _ub: TimeRangeBounds['max']; private _originalInterval: string | null = null; private _i?: moment.Duration | 'auto'; // because other parts of Kibana arbitrarily add properties [key: string]: any; - static __cached__(self: TimeBuckets) { - let cache: any = {}; - const sameMoment = same(moment.isMoment); - const sameDuration = same(moment.isDuration); - - const desc: Record = { - __cached__: { - value: self, - }, - }; - - const breakers: Record = { - setBounds: 'bounds', - clearBounds: 'bounds', - setInterval: 'interval', - }; - - const resources: Record = { - bounds: { - setup() { - return [self._lb, self._ub]; - }, - changes(prev: any) { - return !sameMoment(prev[0], self._lb) || !sameMoment(prev[1], self._ub); - }, - }, - interval: { - setup() { - return self._i; - }, - changes(prev: any) { - return !sameDuration(prev, self._i); - }, - }, - }; - - function cachedGetter(prop: string) { - return { - value: (...rest: any) => { - if (cache.hasOwnProperty(prop)) { - return cache[prop]; - } - - return (cache[prop] = self[prop](...rest)); - }, - }; - } - - function cacheBreaker(prop: string) { - const resource = resources[breakers[prop]]; - const setup = resource.setup; - const changes = resource.changes; - const fn = self[prop]; - - return { - value: (...args: any) => { - const prev = setup.call(self); - const ret = fn.apply(self, ...args); - - if (changes.call(self, prev)) { - cache = {}; - } - - return ret; - }, - }; - } - - function same(checkType: any) { - return function(a: any, b: any) { - if (a === b) return true; - if (checkType(a) === checkType(b)) return +a === +b; - return false; - }; - } - - _.forOwn(TimeBuckets.prototype, (fn, prop) => { - if (!prop || prop[0] === '_') return; - - if (breakers.hasOwnProperty(prop)) { - desc[prop] = cacheBreaker(prop); - } else { - desc[prop] = cachedGetter(prop); - } - }); - - return Object.create(self, desc); - } - - constructor({ uiSettings }: TimeBucketsConfig) { - this.getConfig = (key: string) => uiSettings.get(key); - return TimeBuckets.__cached__(this); + constructor(timeBucketConfig: TimeBucketsConfig) { + this._timeBucketConfig = timeBucketConfig; } /** @@ -182,10 +84,10 @@ export class TimeBuckets { * @return {moment.duration|undefined} */ private getDuration(): moment.Duration | undefined { - if (this._ub === null || this._lb === null || !this.hasBounds()) { + if (this._ub === undefined || this._lb === undefined || !this.hasBounds()) { return; } - const difference = (this._ub as number) - (this._lb as number); + const difference = this._ub.valueOf() - this._lb.valueOf(); return moment.duration(difference, 'ms'); } @@ -200,22 +102,20 @@ export class TimeBuckets { * * @returns {undefined} */ - setBounds(input?: Bounds | Bounds[]) { + setBounds(input?: TimeRangeBounds | TimeRangeBounds[]) { if (!input) return this.clearBounds(); let bounds; - if (_.isPlainObject(input) && !Array.isArray(input)) { + if (isPlainObject(input) && !Array.isArray(input)) { // accept the response from timefilter.getActiveBounds() bounds = [input.min, input.max]; } else { bounds = Array.isArray(input) ? input : []; } - const moments = _(bounds) - .map(_.ary(moment, 1)) - .sortBy(Number); + const moments: Moment[] = sortBy(bounds, Number); - const valid = moments.size() === 2 && moments.every(isValidMoment); + const valid = moments.length === 2 && moments.every(isValidMoment); if (!valid) { this.clearBounds(); throw new Error('invalid bounds set: ' + input); @@ -236,7 +136,7 @@ export class TimeBuckets { * @return {undefined} */ clearBounds() { - this._lb = this._ub = null; + this._lb = this._ub = undefined; } /** @@ -262,7 +162,7 @@ export class TimeBuckets { * object * */ - getBounds(): Bounds | undefined { + getBounds(): TimeRangeBounds | undefined { if (!this.hasBounds()) return; return { min: this._lb, @@ -278,11 +178,10 @@ export class TimeBuckets { * - Any object from src/legacy/ui/agg_types.js * - "auto" * - Pass a valid moment unit - * - a moment.duration object. * * @param {object|string|moment.duration} input - see desc */ - setInterval(input: null | string | Record | moment.Duration) { + setInterval(input: null | string | Record) { let interval = input; // selection object -> val @@ -351,7 +250,7 @@ export class TimeBuckets { const readInterval = () => { const interval = this._i; if (moment.isDuration(interval)) return interval; - return calcAutoIntervalNear(this.getConfig('histogram:barTarget'), Number(duration)); + return calcAutoIntervalNear(this._timeBucketConfig['histogram:barTarget'], Number(duration)); }; const parsedInterval = readInterval(); @@ -362,7 +261,7 @@ export class TimeBuckets { return interval; } - const maxLength: number = this.getConfig('histogram:maxBars'); + const maxLength: number = this._timeBucketConfig['histogram:maxBars']; const approxLen = Number(duration) / Number(interval); let scaled; @@ -396,10 +295,6 @@ export class TimeBuckets { esValue: esInterval.value, esUnit: esInterval.unit, expression: esInterval.expression, - overflow: - Number(duration) > Number(interval) - ? moment.duration(Number(interval) - Number(duration)) - : false, }); }; @@ -423,7 +318,7 @@ export class TimeBuckets { */ getScaledDateFormat() { const interval = this.getInterval(); - const rules = this.getConfig('dateFormat:scaled'); + const rules = this._timeBucketConfig['dateFormat:scaled']; for (let i = rules.length - 1; i >= 0; i--) { const rule = rules[i]; @@ -432,6 +327,6 @@ export class TimeBuckets { } } - return this.getConfig('dateFormat'); + return this._timeBucketConfig.dateFormat; } } diff --git a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts b/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts index 459de66d057d4..9d976784329cc 100644 --- a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts +++ b/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import moment from 'moment'; import { IUiSettingsClient } from 'src/core/public'; import { TimeBuckets } from '../buckets/lib/time_buckets'; import { toAbsoluteDates, TimeRange } from '../../../../common'; @@ -28,12 +28,17 @@ export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { return; } - const buckets = new TimeBuckets({ uiSettings }); + const buckets = new TimeBuckets({ + 'histogram:maxBars': uiSettings.get('histogram:maxBars'), + 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); buckets.setInterval('auto'); buckets.setBounds({ - min: dates.from, - max: dates.to, + min: moment(dates.from), + max: moment(dates.to), }); return buckets.getInterval().expression; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 1687d749f46e2..cce973d632f41 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -54,6 +54,7 @@ export { SearchSourceFields, EsQuerySortValue, SortDirection, + createSearchSource, } from './search_source'; export { SearchInterceptor } from './search_interceptor'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index b70e889066a45..cb1c625a72959 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -33,6 +33,7 @@ export const searchStartMock: jest.Mocked = { aggs: searchAggsStartMock(), setInterceptor: jest.fn(), search: jest.fn(), + createSearchSource: jest.fn(), __LEGACY: { AggConfig: jest.fn() as any, AggType: jest.fn(), diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 42f31ef450d28..6124682184821 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -25,6 +25,8 @@ import { TStrategyTypes } from './strategy_types'; import { getEsClient, LegacyApiCaller } from './es_client'; import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search'; import { esSearchStrategyProvider } from './es_search/es_search_strategy'; +import { IndexPatternsContract } from '../index_patterns/index_patterns'; +import { createSearchSource } from './search_source'; import { QuerySetup } from '../query/query_service'; import { GetInternalStartServicesFn } from '../types'; import { SearchInterceptor } from './search_interceptor'; @@ -108,7 +110,7 @@ export class SearchService implements Plugin { }; } - public start(core: CoreStart): ISearchStart { + public start(core: CoreStart, indexPatterns: IndexPatternsContract): ISearchStart { /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -145,6 +147,7 @@ export class SearchService implements Plugin { // TODO: should an intercepror have a destroy method? this.searchInterceptor = searchInterceptor; }, + createSearchSource: createSearchSource(indexPatterns), __LEGACY: { esClient: this.esClient!, AggConfig, diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts new file mode 100644 index 0000000000000..d49ce5a0d11f8 --- /dev/null +++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts @@ -0,0 +1,151 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { createSearchSource as createSearchSourceFactory } from './create_search_source'; +import { IIndexPattern } from '../../../common/index_patterns'; +import { IndexPatternsContract } from '../../index_patterns/index_patterns'; +import { Filter } from '../../../common/es_query/filters'; + +describe('createSearchSource', function() { + let createSearchSource: ReturnType; + const indexPatternMock: IIndexPattern = {} as IIndexPattern; + let indexPatternContractMock: jest.Mocked; + + beforeEach(() => { + indexPatternContractMock = ({ + get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)), + } as unknown) as jest.Mocked; + createSearchSource = createSearchSourceFactory(indexPatternContractMock); + }); + + it('should fail if JSON is invalid', () => { + expect(createSearchSource('{', [])).rejects.toThrow(); + expect(createSearchSource('0', [])).rejects.toThrow(); + expect(createSearchSource('"abcdefg"', [])).rejects.toThrow(); + }); + + it('should set fields', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + highlightAll: true, + query: { + query: '', + language: 'kuery', + }, + }), + [] + ); + expect(searchSource.getOwnField('highlightAll')).toBe(true); + expect(searchSource.getOwnField('query')).toEqual({ + query: '', + language: 'kuery', + }); + }); + + it('should resolve referenced index pattern', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }), + [ + { + id: '123-456', + type: 'index-pattern', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }, + ] + ); + expect(indexPatternContractMock.get).toHaveBeenCalledWith('123-456'); + expect(searchSource.getOwnField('index')).toBe(indexPatternMock); + }); + + it('should set filters and resolve referenced index patterns', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + filter: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Clothing", + }, + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + query: { + match_phrase: { + 'category.keyword': "Men's Clothing", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }), + [ + { + id: '123-456', + type: 'index-pattern', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + ] + ); + const filters = searchSource.getOwnField('filter') as Filter[]; + expect(filters[0]).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "123-456", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Clothing", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Clothing", + }, + }, + } + `); + }); + + it('should migrate legacy queries on the fly', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + highlightAll: true, + query: 'a:b', + }), + [] + ); + expect(searchSource.getOwnField('query')).toEqual({ + query: 'a:b', + language: 'lucene', + }); + }); +}); diff --git a/src/plugins/data/public/search/search_source/create_search_source.ts b/src/plugins/data/public/search/search_source/create_search_source.ts new file mode 100644 index 0000000000000..35b7ac4eb9762 --- /dev/null +++ b/src/plugins/data/public/search/search_source/create_search_source.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { SavedObjectReference } from 'kibana/public'; +import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; +import { InvalidJSONProperty } from '../../../../kibana_utils/public'; +import { SearchSource } from './search_source'; +import { IndexPatternsContract } from '../../index_patterns/index_patterns'; +import { SearchSourceFields } from './types'; + +/** + * Deserializes a json string and a set of referenced objects to a `SearchSource` instance. + * Use this method to re-create the search source serialized using `searchSource.serialize`. + * + * This function is a factory function that returns the actual utility when calling it with the + * required service dependency (index patterns contract). A pre-wired version is also exposed in + * the start contract of the data plugin as part of the search service + * + * @param indexPatterns The index patterns contract of the data plugin + * + * @return Wired utility function taking two parameters `searchSourceJson`, the json string + * returned by `serializeSearchSource` and `references`, a list of references including the ones + * returned by `serializeSearchSource`. + * + * @public */ +export const createSearchSource = (indexPatterns: IndexPatternsContract) => async ( + searchSourceJson: string, + references: SavedObjectReference[] +) => { + const searchSource = new SearchSource(); + + // if we have a searchSource, set its values based on the searchSourceJson field + let searchSourceValues: Record; + try { + searchSourceValues = JSON.parse(searchSourceJson); + } catch (e) { + throw new InvalidJSONProperty( + `Invalid JSON in search source. ${e.message} JSON: ${searchSourceJson}` + ); + } + + // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. + // (This happened in issue #20308) + if (!searchSourceValues || typeof searchSourceValues !== 'object') { + throw new InvalidJSONProperty('Invalid JSON in search source.'); + } + + // Inject index id if a reference is saved + if (searchSourceValues.indexRefName) { + const reference = references.find(ref => ref.name === searchSourceValues.indexRefName); + if (!reference) { + throw new Error(`Could not find reference for ${searchSourceValues.indexRefName}`); + } + searchSourceValues.index = reference.id; + delete searchSourceValues.indexRefName; + } + + if (searchSourceValues.filter && Array.isArray(searchSourceValues.filter)) { + searchSourceValues.filter.forEach((filterRow: any) => { + if (!filterRow.meta || !filterRow.meta.indexRefName) { + return; + } + const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); + if (!reference) { + throw new Error(`Could not find reference for ${filterRow.meta.indexRefName}`); + } + filterRow.meta.index = reference.id; + delete filterRow.meta.indexRefName; + }); + } + + if (searchSourceValues.index && typeof searchSourceValues.index === 'string') { + searchSourceValues.index = await indexPatterns.get(searchSourceValues.index); + } + + const searchSourceFields = searchSource.getFields(); + const fnProps = _.transform( + searchSourceFields, + function(dynamic, val, name) { + if (_.isFunction(val) && name) dynamic[name] = val; + }, + {} + ); + + // This assignment might hide problems because the type of values passed from the parsed JSON + // might not fit the SearchSourceFields interface. + const newFields: SearchSourceFields = _.defaults(searchSourceValues, fnProps); + + searchSource.setFields(newFields); + const query = searchSource.getOwnField('query'); + + if (typeof query !== 'undefined') { + searchSource.setField('query', migrateLegacyQuery(query)); + } + + return searchSource; +}; diff --git a/src/plugins/data/public/search/search_source/index.ts b/src/plugins/data/public/search/search_source/index.ts index 10f1b2bc332e1..0e9f530d0968a 100644 --- a/src/plugins/data/public/search/search_source/index.ts +++ b/src/plugins/data/public/search/search_source/index.ts @@ -18,4 +18,5 @@ */ export * from './search_source'; +export { createSearchSource } from './create_search_source'; export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types'; diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index 700bea741bd6a..1ef7c1187a9e0 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -37,4 +37,5 @@ export const searchSourceMock: MockedKeys = { getSearchRequestBody: jest.fn(), destroy: jest.fn(), history: [], + serialize: jest.fn(), }; diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index fcd116a3f4121..6bad093d31402 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -18,7 +18,7 @@ */ import { SearchSource } from './search_source'; -import { IndexPattern } from '../..'; +import { IndexPattern, SortDirection } from '../..'; import { mockDataServices } from '../aggs/test_helpers'; jest.mock('../fetch', () => ({ @@ -150,4 +150,77 @@ describe('SearchSource', function() { expect(parentFn).toBeCalledWith(searchSource, options); }); }); + + describe('#serialize', function() { + it('should reference index patterns', () => { + const indexPattern123 = { id: '123' } as IndexPattern; + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern123); + const { searchSourceJSON, references } = searchSource.serialize(); + expect(references[0].id).toEqual('123'); + expect(references[0].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name); + }); + + it('should add other fields', () => { + const searchSource = new SearchSource(); + searchSource.setField('highlightAll', true); + searchSource.setField('from', 123456); + const { searchSourceJSON } = searchSource.serialize(); + expect(JSON.parse(searchSourceJSON).highlightAll).toEqual(true); + expect(JSON.parse(searchSourceJSON).from).toEqual(123456); + }); + + it('should omit sort and size', () => { + const searchSource = new SearchSource(); + searchSource.setField('highlightAll', true); + searchSource.setField('from', 123456); + searchSource.setField('sort', { field: SortDirection.asc }); + searchSource.setField('size', 200); + const { searchSourceJSON } = searchSource.serialize(); + expect(Object.keys(JSON.parse(searchSourceJSON))).toEqual(['highlightAll', 'from']); + }); + + it('should serialize filters', () => { + const searchSource = new SearchSource(); + const filter = [ + { + query: 'query', + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ]; + searchSource.setField('filter', filter); + const { searchSourceJSON } = searchSource.serialize(); + expect(JSON.parse(searchSourceJSON).filter).toEqual(filter); + }); + + it('should reference index patterns in filters separately from index field', () => { + const searchSource = new SearchSource(); + const indexPattern123 = { id: '123' } as IndexPattern; + searchSource.setField('index', indexPattern123); + const filter = [ + { + query: 'query', + meta: { + alias: 'alias', + disabled: false, + negate: false, + index: '456', + }, + }, + ]; + searchSource.setField('filter', filter); + const { searchSourceJSON, references } = searchSource.serialize(); + expect(references[0].id).toEqual('123'); + expect(references[0].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name); + expect(references[1].id).toEqual('456'); + expect(references[1].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).filter[0].meta.indexRefName).toEqual(references[1].name); + }); + }); }); diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 0c3321f03dabc..c70db7bb82ef7 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -70,6 +70,7 @@ */ import _ from 'lodash'; +import { SavedObjectReference } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; @@ -419,4 +420,85 @@ export class SearchSource { return searchRequest; } + + /** + * Serializes the instance to a JSON string and a set of referenced objects. + * Use this method to get a representation of the search source which can be stored in a saved object. + * + * The references returned by this function can be mixed with other references in the same object, + * however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` + * and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. + * + * Using `createSearchSource`, the instance can be re-created. + * @param searchSource The search source to serialize + * @public */ + public serialize() { + const references: SavedObjectReference[] = []; + + const { + filter: originalFilters, + ...searchSourceFields + }: Omit = _.omit(this.getFields(), ['sort', 'size']); + let serializedSearchSourceFields: Omit & { + indexRefName?: string; + filter?: Array & { meta: Filter['meta'] & { indexRefName?: string } }>; + } = searchSourceFields; + if (searchSourceFields.index) { + const indexId = searchSourceFields.index.id!; + const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + references.push({ + name: refName, + type: 'index-pattern', + id: indexId, + }); + serializedSearchSourceFields = { + ...serializedSearchSourceFields, + indexRefName: refName, + index: undefined, + }; + } + if (originalFilters) { + const filters = this.getFilters(originalFilters); + serializedSearchSourceFields = { + ...serializedSearchSourceFields, + filter: filters.map((filterRow, i) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }), + }; + } + + return { searchSourceJSON: JSON.stringify(serializedSearchSourceFields), references }; + } + + private getFilters(filterField: SearchSourceFields['filter']): Filter[] { + if (!filterField) { + return []; + } + + if (Array.isArray(filterField)) { + return filterField; + } + + if (_.isFunction(filterField)) { + return this.getFilters(filterField()); + } + + return [filterField]; + } } diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 03cbfa9f8ed84..ba6e44f47b75e 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -18,6 +18,7 @@ */ import { CoreStart } from 'kibana/public'; +import { createSearchSource } from './search_source'; import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs'; import { ISearch, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; @@ -89,5 +90,6 @@ export interface ISearchStart { aggs: SearchAggsStart; setInterceptor: (searchInterceptor: SearchInterceptor) => void; search: ISearchGeneric; + createSearchSource: ReturnType; __LEGACY: ISearchStartLegacy & SearchAggsStartLegacy; } diff --git a/src/plugins/discover/public/services.ts b/src/plugins/discover/public/services.ts index 3a28759d82b71..37e2144800ea1 100644 --- a/src/plugins/discover/public/services.ts +++ b/src/plugins/discover/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../kibana_utils/common'; +import { createGetterSetter } from '../../kibana_utils/public'; import { DocViewsRegistry } from './doc_views/doc_views_registry'; export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter( diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts index 56facc37fc666..ddd84b0544345 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts @@ -21,7 +21,7 @@ import { Embeddable } from './embeddable'; import { EmbeddableInput } from './i_embeddable'; import { ViewMode } from '../types'; import { EmbeddableActionStorage, SerializedEvent } from './embeddable_action_storage'; -import { of } from '../../../../kibana_utils/common'; +import { of } from '../../../../kibana_utils/public'; class TestEmbeddable extends Embeddable { public readonly type = 'test'; diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json new file mode 100644 index 0000000000000..5a3db3b344090 --- /dev/null +++ b/src/plugins/es_ui_shared/kibana.json @@ -0,0 +1,5 @@ +{ + "id": "esUiShared", + "version": "kibana", + "ui": true +} diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 944b800c66a28..6db6248f4c68f 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -35,3 +35,11 @@ export { export { indices } from './indices'; export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode'; + +/** dummy plugin, we just want esUiShared to have its own bundle */ +export function plugin() { + return new (class EsUiSharedPlugin { + setup() {} + start() {} + })(); +} diff --git a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts index 4c85ad3985b3d..2a6cfa0358709 100644 --- a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createAuditbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -305,13 +306,7 @@ export const createAuditbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, DEB: { title: i18n.translate('home.tutorials.common.auditbeatCloudInstructions.config.debTitle', { @@ -327,13 +322,7 @@ export const createAuditbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatCloudInstructions.config.debTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, RPM: { title: i18n.translate('home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle', { @@ -349,13 +338,7 @@ export const createAuditbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate( @@ -374,13 +357,7 @@ export const createAuditbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts new file mode 100644 index 0000000000000..a18e21d2b43dd --- /dev/null +++ b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { i18n } from '@kbn/i18n'; + +export const cloudPasswordAndResetLink = i18n.translate( + 'home.tutorials.common.cloudInstructions.passwordAndResetLink', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user.' + + `\\{#config.cloud.resetPasswordUrl\\} + Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.resetPasswordUrl\\}). + \\{/config.cloud.resetPasswordUrl\\}`, + values: { passwordTemplate: '``' }, + } +); diff --git a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts index 66efa36ec9bcd..0e99033b2ea69 100644 --- a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createFilebeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -299,13 +300,7 @@ export const createFilebeatCloudInstructions = () => ({ }, }), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, DEB: { title: i18n.translate('home.tutorials.common.filebeatCloudInstructions.config.debTitle', { @@ -318,13 +313,7 @@ export const createFilebeatCloudInstructions = () => ({ }, }), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatCloudInstructions.config.debTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, RPM: { title: i18n.translate('home.tutorials.common.filebeatCloudInstructions.config.rpmTitle', { @@ -337,13 +326,7 @@ export const createFilebeatCloudInstructions = () => ({ }, }), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatCloudInstructions.config.rpmTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate('home.tutorials.common.filebeatCloudInstructions.config.windowsTitle', { @@ -359,13 +342,7 @@ export const createFilebeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts index ee13b9c5eefd8..06ff84146b5d8 100644 --- a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -200,13 +201,7 @@ export const createFunctionbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.functionbeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate( @@ -225,13 +220,7 @@ export const createFunctionbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts index 33f5defc0273f..fa5bf5df13b6b 100644 --- a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createHeartbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -280,13 +281,7 @@ export const createHeartbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, DEB: { title: i18n.translate('home.tutorials.common.heartbeatCloudInstructions.config.debTitle', { @@ -302,13 +297,7 @@ export const createHeartbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatCloudInstructions.config.debTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, RPM: { title: i18n.translate('home.tutorials.common.heartbeatCloudInstructions.config.rpmTitle', { @@ -324,13 +313,7 @@ export const createHeartbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate( @@ -349,13 +332,7 @@ export const createHeartbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts index 9fdc70e0703a4..651405941610f 100644 --- a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createMetricbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -295,13 +296,7 @@ export const createMetricbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, DEB: { title: i18n.translate('home.tutorials.common.metricbeatCloudInstructions.config.debTitle', { @@ -317,13 +312,7 @@ export const createMetricbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatCloudInstructions.config.debTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, RPM: { title: i18n.translate('home.tutorials.common.metricbeatCloudInstructions.config.rpmTitle', { @@ -339,13 +328,7 @@ export const createMetricbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate( @@ -364,13 +347,7 @@ export const createMetricbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts index 9d7d0660d3d6c..27d7822e080a3 100644 --- a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createWinlogbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -130,13 +131,7 @@ export const createWinlogbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json new file mode 100644 index 0000000000000..d5397a11184aa --- /dev/null +++ b/src/plugins/index_pattern_management/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "indexPatternManagement", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [] +} diff --git a/src/legacy/core_plugins/management/public/np_ready/index.ts b/src/plugins/index_pattern_management/public/index.ts similarity index 83% rename from src/legacy/core_plugins/management/public/np_ready/index.ts rename to src/plugins/index_pattern_management/public/index.ts index bae0f1d3e23cd..da482c0c51f0a 100644 --- a/src/legacy/core_plugins/management/public/np_ready/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -29,14 +29,11 @@ * either types, or static code. */ import { PluginInitializerContext } from 'src/core/public'; -import { ManagementPlugin } from './plugin'; -export { ManagementSetup, ManagementStart } from './plugin'; +import { IndexPatternManagementPlugin } from './plugin'; +export { IndexPatternManagementSetup, IndexPatternManagementStart } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { - return new ManagementPlugin(initializerContext); + return new IndexPatternManagementPlugin(initializerContext); } -export { - IndexPatternCreationConfig, - IndexPatternListConfig, -} from './services/index_pattern_management'; +export { IndexPatternCreationConfig, IndexPatternListConfig } from './service'; diff --git a/src/legacy/core_plugins/management/public/np_ready/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts similarity index 57% rename from src/legacy/core_plugins/management/public/np_ready/mocks.ts rename to src/plugins/index_pattern_management/public/mocks.ts index ae0be98de63f3..bc97f46c302e3 100644 --- a/src/legacy/core_plugins/management/public/np_ready/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -18,42 +18,38 @@ */ import { PluginInitializerContext } from 'src/core/public'; -import { coreMock } from '../../../../../core/public/mocks'; +import { coreMock } from '../../../core/public/mocks'; import { - ManagementSetup, - ManagementStart, - ManagementPlugin, - ManagementPluginSetupDependencies, + IndexPatternManagementSetup, + IndexPatternManagementStart, + IndexPatternManagementPlugin, } from './plugin'; -const createSetupContract = (): ManagementSetup => ({ - indexPattern: { - creation: { - add: jest.fn(), - getType: jest.fn(), - getIndexPatternCreationOptions: jest.fn(), - } as any, - list: { - add: jest.fn(), - getIndexPatternTags: jest.fn(), - getFieldInfo: jest.fn(), - areScriptedFieldsEnabled: jest.fn(), - } as any, - }, +const createSetupContract = (): IndexPatternManagementSetup => ({ + creation: { + addCreationConfig: jest.fn(), + } as any, + list: { + addListConfig: jest.fn(), + } as any, }); -const createStartContract = (): ManagementStart => ({}); +const createStartContract = (): IndexPatternManagementStart => ({ + creation: { + getType: jest.fn(), + getIndexPatternCreationOptions: jest.fn(), + } as any, + list: { + getIndexPatternTags: jest.fn(), + getFieldInfo: jest.fn(), + areScriptedFieldsEnabled: jest.fn(), + } as any, +}); const createInstance = async () => { - const plugin = new ManagementPlugin({} as PluginInitializerContext); + const plugin = new IndexPatternManagementPlugin({} as PluginInitializerContext); - const setup = plugin.setup(coreMock.createSetup(), ({ - home: { - featureCatalogue: { - register: jest.fn(), - }, - }, - } as unknown) as ManagementPluginSetupDependencies); + const setup = plugin.setup(coreMock.createSetup()); const doStart = () => plugin.start(coreMock.createStart(), {}); return { diff --git a/src/legacy/core_plugins/management/public/np_ready/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts similarity index 60% rename from src/legacy/core_plugins/management/public/np_ready/plugin.ts rename to src/plugins/index_pattern_management/public/plugin.ts index 2a8ef10c817cc..93bb0ead1df4a 100644 --- a/src/legacy/core_plugins/management/public/np_ready/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -17,43 +17,40 @@ * under the License. */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { HomePublicPluginSetup } from 'src/plugins/home/public'; -import { IndexPatternManagementService, IndexPatternManagementSetup } from './services'; +import { + IndexPatternManagementService, + IndexPatternManagementServiceSetup, + IndexPatternManagementServiceStart, +} from './service'; -export interface ManagementPluginSetupDependencies { - home: HomePublicPluginSetup; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IndexPatternManagementSetupDependencies {} // eslint-disable-next-line @typescript-eslint/no-empty-interface -interface ManagementPluginStartDependencies {} +export interface IndexPatternManagementStartDependencies {} -export interface ManagementSetup { - indexPattern: IndexPatternManagementSetup; -} +export type IndexPatternManagementSetup = IndexPatternManagementServiceSetup; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ManagementStart {} +export type IndexPatternManagementStart = IndexPatternManagementServiceStart; -export class ManagementPlugin +export class IndexPatternManagementPlugin implements Plugin< - ManagementSetup, - ManagementStart, - ManagementPluginSetupDependencies, - ManagementPluginStartDependencies + IndexPatternManagementSetup, + IndexPatternManagementStart, + IndexPatternManagementSetupDependencies, + IndexPatternManagementStartDependencies > { private readonly indexPattern = new IndexPatternManagementService(); constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { home }: ManagementPluginSetupDependencies) { - return { - indexPattern: this.indexPattern.setup({ httpClient: core.http, home }), - }; + public setup(core: CoreSetup) { + return this.indexPattern.setup({ httpClient: core.http }); } - public start(core: CoreStart, plugins: ManagementPluginStartDependencies) { - return {}; + public start(core: CoreStart, plugins: IndexPatternManagementStartDependencies) { + return this.indexPattern.start(); } public stop() { diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts b/src/plugins/index_pattern_management/public/service/creation/config.ts similarity index 88% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts rename to src/plugins/index_pattern_management/public/service/creation/config.ts index 5714fa3338962..29ab0ebfc3d5f 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts +++ b/src/plugins/index_pattern_management/public/service/creation/config.ts @@ -18,20 +18,20 @@ */ import { i18n } from '@kbn/i18n'; -import { MatchedIndex } from '../../../../../../kibana/public/management/sections/index_patterns/create_index_pattern_wizard/types'; +import { MatchedIndex } from '../../../../../legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/types'; const indexPatternTypeName = i18n.translate( - 'management.editIndexPattern.createIndex.defaultTypeName', + 'indexPatternManagement.editIndexPattern.createIndex.defaultTypeName', { defaultMessage: 'index pattern' } ); const indexPatternButtonText = i18n.translate( - 'management.editIndexPattern.createIndex.defaultButtonText', + 'indexPatternManagement.editIndexPattern.createIndex.defaultButtonText', { defaultMessage: 'Standard index pattern' } ); const indexPatternButtonDescription = i18n.translate( - 'management.editIndexPattern.createIndex.defaultButtonDescription', + 'indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription', { defaultMessage: 'Perform full aggregations against any data' } ); diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/index.ts b/src/plugins/index_pattern_management/public/service/creation/index.ts similarity index 100% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/index.ts rename to src/plugins/index_pattern_management/public/service/creation/index.ts diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts b/src/plugins/index_pattern_management/public/service/creation/manager.ts similarity index 79% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts rename to src/plugins/index_pattern_management/public/service/creation/manager.ts index e7fa13409ab04..32b3e7ee7a133 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts +++ b/src/plugins/index_pattern_management/public/service/creation/manager.ts @@ -17,23 +17,25 @@ * under the License. */ -import { HttpSetup } from '../../../../../../../../core/public'; +import { HttpSetup } from '../../../../../core/public'; import { IndexPatternCreationConfig, UrlHandler, IndexPatternCreationOption } from './config'; export class IndexPatternCreationManager { private configs: IndexPatternCreationConfig[]; - constructor(private readonly httpClient: HttpSetup) { + constructor() { this.configs = []; } - public add(Config: typeof IndexPatternCreationConfig) { - const config = new Config({ httpClient: this.httpClient }); + public addCreationConfig = (httpClient: HttpSetup) => ( + Config: typeof IndexPatternCreationConfig + ) => { + const config = new Config({ httpClient }); if (this.configs.findIndex(c => c.key === config.key) !== -1) { throw new Error(`${config.key} exists in IndexPatternCreationManager.`); } this.configs.push(config); - } + }; public getType(key: string | undefined): IndexPatternCreationConfig | null { if (key) { @@ -58,4 +60,13 @@ export class IndexPatternCreationManager { ); return options; } + + setup = (httpClient: HttpSetup) => ({ + addCreationConfig: this.addCreationConfig(httpClient).bind(this), + }); + + start = () => ({ + getType: this.getType.bind(this), + getIndexPatternCreationOptions: this.getIndexPatternCreationOptions.bind(this), + }); } diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index.ts b/src/plugins/index_pattern_management/public/service/index.ts similarity index 100% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index.ts rename to src/plugins/index_pattern_management/public/service/index.ts diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts similarity index 51% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts rename to src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index 2b6f008dd928a..4780fa00ed468 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -17,18 +17,12 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -import { - FeatureCatalogueCategory, - HomePublicPluginSetup, -} from '../../../../../../../plugins/home/public'; -import { HttpSetup } from '../../../../../../../core/public'; +import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; interface SetupDependencies { httpClient: HttpSetup; - home: HomePublicPluginSetup; } /** @@ -37,31 +31,29 @@ interface SetupDependencies { * @internal */ export class IndexPatternManagementService { - public setup({ httpClient, home }: SetupDependencies) { - const creation = new IndexPatternCreationManager(httpClient); - const list = new IndexPatternListManager(); + indexPatternCreationManager: IndexPatternCreationManager; + indexPatternListConfig: IndexPatternListManager; - creation.add(IndexPatternCreationConfig); - list.add(IndexPatternListConfig); + constructor() { + this.indexPatternCreationManager = new IndexPatternCreationManager(); + this.indexPatternListConfig = new IndexPatternListManager(); + } + + public setup({ httpClient }: SetupDependencies) { + const creationManagerSetup = this.indexPatternCreationManager.setup(httpClient); + creationManagerSetup.addCreationConfig(IndexPatternCreationConfig); + this.indexPatternListConfig.setup().addListConfig(IndexPatternListConfig); - home.featureCatalogue.register({ - id: 'index_patterns', - title: i18n.translate('management.indexPatternHeader', { - defaultMessage: 'Index Patterns', - }), - description: i18n.translate('management.indexPatternLabel', { - defaultMessage: - 'Manage the index patterns that help retrieve your data from Elasticsearch.', - }), - icon: 'indexPatternApp', - path: '/app/kibana#/management/kibana/index_patterns', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }); + return { + creation: creationManagerSetup, + list: this.indexPatternListConfig.setup(), + }; + } + public start() { return { - creation, - list, + creation: this.indexPatternCreationManager.start(), + list: this.indexPatternListConfig.start(), }; } @@ -71,4 +63,5 @@ export class IndexPatternManagementService { } /** @internal */ -export type IndexPatternManagementSetup = ReturnType; +export type IndexPatternManagementServiceSetup = ReturnType; +export type IndexPatternManagementServiceStart = ReturnType; diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/config.ts b/src/plugins/index_pattern_management/public/service/list/config.ts similarity index 87% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/config.ts rename to src/plugins/index_pattern_management/public/service/list/config.ts index dd4d77a681171..87c246e8913e5 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/config.ts +++ b/src/plugins/index_pattern_management/public/service/list/config.ts @@ -33,9 +33,12 @@ export class IndexPatternListConfig { ? [ { key: 'default', - name: i18n.translate('management.editIndexPattern.list.defaultIndexPatternListName', { - defaultMessage: 'Default', - }), + name: i18n.translate( + 'indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName', + { + defaultMessage: 'Default', + } + ), }, ] : []; diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/index.ts b/src/plugins/index_pattern_management/public/service/list/index.ts similarity index 100% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/index.ts rename to src/plugins/index_pattern_management/public/service/list/index.ts diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/manager.ts b/src/plugins/index_pattern_management/public/service/list/manager.ts similarity index 75% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/manager.ts rename to src/plugins/index_pattern_management/public/service/list/manager.ts index 73ca33ae914a9..3a2910a222cd7 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/manager.ts +++ b/src/plugins/index_pattern_management/public/service/list/manager.ts @@ -27,7 +27,7 @@ export class IndexPatternListManager { this.configs = []; } - public add(Config: typeof IndexPatternListConfig) { + private addListConfig(Config: typeof IndexPatternListConfig) { const config = new Config(); if (this.configs.findIndex(c => c.key === config.key) !== -1) { throw new Error(`${config.key} exists in IndexPatternListManager.`); @@ -35,7 +35,7 @@ export class IndexPatternListManager { this.configs.push(config); } - public getIndexPatternTags(indexPattern: IIndexPattern, isDefault: boolean) { + private getIndexPatternTags(indexPattern: IIndexPattern, isDefault: boolean) { return this.configs.reduce((tags: IndexPatternTag[], config) => { return config.getIndexPatternTags ? tags.concat(config.getIndexPatternTags(indexPattern, isDefault)) @@ -43,15 +43,25 @@ export class IndexPatternListManager { }, []); } - public getFieldInfo(indexPattern: IIndexPattern, field: IFieldType): string[] { + private getFieldInfo(indexPattern: IIndexPattern, field: IFieldType): string[] { return this.configs.reduce((info: string[], config) => { return config.getFieldInfo ? info.concat(config.getFieldInfo(indexPattern, field)) : info; }, []); } - public areScriptedFieldsEnabled(indexPattern: IIndexPattern): boolean { + private areScriptedFieldsEnabled(indexPattern: IIndexPattern): boolean { return this.configs.every(config => { return config.areScriptedFieldsEnabled ? config.areScriptedFieldsEnabled(indexPattern) : true; }); } + + setup = () => ({ + addListConfig: this.addListConfig.bind(this), + }); + + start = () => ({ + getIndexPatternTags: this.getIndexPatternTags.bind(this), + getFieldInfo: this.getFieldInfo.bind(this), + areScriptedFieldsEnabled: this.areScriptedFieldsEnabled.bind(this), + }); } diff --git a/src/plugins/kibana_react/kibana.json b/src/plugins/kibana_react/kibana.json new file mode 100644 index 0000000000000..0add1bee84ae0 --- /dev/null +++ b/src/plugins/kibana_react/kibana.json @@ -0,0 +1,5 @@ +{ + "id": "kibanaReact", + "version": "kibana", + "ui": true +} diff --git a/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts b/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts index b4007b30cf8ca..21bba92ada4c1 100644 --- a/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts +++ b/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts @@ -19,7 +19,7 @@ import { ComponentType, createElement as h } from 'react'; import { render as renderReact, unmountComponentAtNode } from 'react-dom'; -import { UiComponent, UiComponentInstance } from '../../../kibana_utils/common'; +import { UiComponent, UiComponentInstance } from '../../../kibana_utils/public'; /** * Transform a React component into a `UiComponent`. diff --git a/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx b/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx index 939d372b9997f..aefbd66e50fcf 100644 --- a/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx +++ b/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { UiComponent } from '../../../kibana_utils/common'; +import { UiComponent } from '../../../kibana_utils/public'; import { uiToReactComponent } from './ui_to_react_component'; import { reactToUiComponent } from './react_to_ui_component'; diff --git a/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts b/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts index 9b34880cf4fe3..ee99ea3672672 100644 --- a/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts +++ b/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts @@ -18,7 +18,7 @@ */ import { FC, createElement as h, useRef, useLayoutEffect, useMemo } from 'react'; -import { UiComponent, UiComponentInstance } from '../../../kibana_utils/common'; +import { UiComponent, UiComponentInstance } from '../../../kibana_utils/public'; /** * Transforms `UiComponent` into a React component. diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index e1689e38dbfe0..9ad9f14ac5659 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -31,3 +31,11 @@ export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; export { toMountPoint } from './util'; + +/** dummy plugin, we just want kibanaReact to have its own bundle */ +export function plugin() { + return new (class KibanaReactPlugin { + setup() {} + start() {} + })(); +} diff --git a/src/plugins/kibana_utils/kibana.json b/src/plugins/kibana_utils/kibana.json new file mode 100644 index 0000000000000..6fa39d82d1021 --- /dev/null +++ b/src/plugins/kibana_utils/kibana.json @@ -0,0 +1,5 @@ +{ + "id": "kibanaUtils", + "version": "kibana", + "ui": true +} diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 1876e688c989a..2f139050e994a 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -19,7 +19,6 @@ export { calculateObjectHash, - createGetterSetter, defer, Defer, Get, @@ -31,6 +30,8 @@ export { UiComponent, UiComponentInstance, url, + createGetterSetter, + defaultFeedbackMessage, } from '../common'; export * from './core'; export * from './errors'; @@ -75,3 +76,11 @@ export { } from './state_sync'; export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; export { applyDiff } from './state_management/utils/diff_object'; + +/** dummy plugin, we just want kibanaUtils to have its own bundle */ +export function plugin() { + return new (class KibanaUtilsPlugin { + setup() {} + start() {} + })(); +} diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index 0f5773c00283e..7927238e12066 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -39,6 +39,7 @@ export class SavedObjectsPublicPlugin SavedObjectClass: createSavedObjectClass({ indexPatterns: data.indexPatterns, savedObjectsClient: core.savedObjects.client, + search: data.search, chrome: core.chrome, overlays: core.overlays, }), diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts index 2e965eaf1989b..9776887b6d741 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts @@ -18,9 +18,8 @@ */ import _ from 'lodash'; import { EsResponse, SavedObject, SavedObjectConfig } from '../../types'; -import { parseSearchSource } from './parse_search_source'; import { expandShorthand, SavedObjectNotFound } from '../../../../kibana_utils/public'; -import { IndexPattern } from '../../../../data/public'; +import { DataPublicPluginStart, IndexPattern } from '../../../../data/public'; /** * A given response of and ElasticSearch containing a plain saved object is applied to the given @@ -29,13 +28,13 @@ import { IndexPattern } from '../../../../data/public'; export async function applyESResp( resp: EsResponse, savedObject: SavedObject, - config: SavedObjectConfig + config: SavedObjectConfig, + createSearchSource: DataPublicPluginStart['search']['createSearchSource'] ) { const mapping = expandShorthand(config.mapping); const esType = config.type || ''; savedObject._source = _.cloneDeep(resp._source); const injectReferences = config.injectReferences; - const hydrateIndexPattern = savedObject.hydrateIndexPattern!; if (typeof resp.found === 'boolean' && !resp.found) { throw new SavedObjectNotFound(esType, savedObject.id || ''); } @@ -64,13 +63,34 @@ export async function applyESResp( _.assign(savedObject, savedObject._source); savedObject.lastSavedTitle = savedObject.title; - await parseSearchSource(savedObject, esType, meta.searchSourceJSON, resp.references); - await hydrateIndexPattern(); + if (config.searchSource) { + try { + savedObject.searchSource = await createSearchSource(meta.searchSourceJSON, resp.references); + } catch (error) { + if ( + error.constructor.name === 'SavedObjectNotFound' && + error.savedObjectType === 'index-pattern' + ) { + // if parsing the search source fails because the index pattern wasn't found, + // remember the reference - this is required for error handling on legacy imports + savedObject.unresolvedIndexPatternReference = { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + id: JSON.parse(meta.searchSourceJSON).index, + type: 'index-pattern', + }; + } + + throw error; + } + } + if (injectReferences && resp.references && resp.references.length > 0) { injectReferences(savedObject, resp.references); } + if (typeof config.afterESResp === 'function') { savedObject = await config.afterESResp(savedObject); } + return savedObject; } diff --git a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts index b9043890e2775..e8faef4e9e040 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts @@ -81,7 +81,8 @@ export function buildSavedObject( */ savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); - savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config); + savedObject.applyESResp = (resp: EsResponse) => + applyESResp(resp, savedObject, config, services.search.createSearchSource); /** * Serialize this object diff --git a/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts b/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts index b55538e4073ba..84275cf35befb 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts @@ -31,25 +31,19 @@ export async function hydrateIndexPattern( indexPatterns: IndexPatternsContract, config: SavedObjectConfig ) { - const clearSavedIndexPattern = !!config.clearSavedIndexPattern; const indexPattern = config.indexPattern; if (!savedObject.searchSource) { return null; } - if (clearSavedIndexPattern) { - savedObject.searchSource!.setField('index', undefined); - return null; - } - - const index = id || indexPattern || savedObject.searchSource!.getOwnField('index'); + const index = id || indexPattern || savedObject.searchSource.getOwnField('index'); if (typeof index !== 'string' || !index) { return null; } const indexObj = await indexPatterns.get(index); - savedObject.searchSource!.setField('index', indexObj); + savedObject.searchSource.setField('index', indexObj); return indexObj; } diff --git a/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts b/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts deleted file mode 100644 index cdb191f9e7df8..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import _ from 'lodash'; -import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; -import { SavedObject } from '../../types'; -import { InvalidJSONProperty } from '../../../../kibana_utils/public'; - -export function parseSearchSource( - savedObject: SavedObject, - esType: string, - searchSourceJson: string, - references: any[] -) { - if (!savedObject.searchSource) return; - - // if we have a searchSource, set its values based on the searchSourceJson field - let searchSourceValues: Record; - try { - searchSourceValues = JSON.parse(searchSourceJson); - } catch (e) { - throw new InvalidJSONProperty( - `Invalid JSON in ${esType} "${savedObject.id}". ${e.message} JSON: ${searchSourceJson}` - ); - } - - // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. - // (This happened in issue #20308) - if (!searchSourceValues || typeof searchSourceValues !== 'object') { - throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${savedObject.id}".`); - } - - // Inject index id if a reference is saved - if (searchSourceValues.indexRefName) { - const reference = references.find( - (ref: Record) => ref.name === searchSourceValues.indexRefName - ); - if (!reference) { - throw new Error( - `Could not find reference for ${ - searchSourceValues.indexRefName - } on ${savedObject.getEsType()} ${savedObject.id}` - ); - } - searchSourceValues.index = reference.id; - delete searchSourceValues.indexRefName; - } - - if (searchSourceValues.filter) { - searchSourceValues.filter.forEach((filterRow: any) => { - if (!filterRow.meta || !filterRow.meta.indexRefName) { - return; - } - const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); - if (!reference) { - throw new Error( - `Could not find reference for ${ - filterRow.meta.indexRefName - } on ${savedObject.getEsType()}` - ); - } - filterRow.meta.index = reference.id; - delete filterRow.meta.indexRefName; - }); - } - - const searchSourceFields = savedObject.searchSource.getFields(); - const fnProps = _.transform( - searchSourceFields, - function(dynamic: Record, val: any, name: string | undefined) { - if (_.isFunction(val) && name) dynamic[name] = val; - }, - {} - ); - - savedObject.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); - const query = savedObject.searchSource.getOwnField('query'); - - if (typeof query !== 'undefined') { - savedObject.searchSource.setField('query', migrateLegacyQuery(query)); - } -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts index 8a020ca03aea3..78f9eeb8b5fb1 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts @@ -17,7 +17,6 @@ * under the License. */ import _ from 'lodash'; -import angular from 'angular'; import { SavedObject, SavedObjectConfig } from '../../types'; import { expandShorthand } from '../../../../kibana_utils/public'; @@ -41,57 +40,16 @@ export function serializeSavedObject(savedObject: SavedObject, config: SavedObje }); if (savedObject.searchSource) { - let searchSourceFields: Record = _.omit(savedObject.searchSource.getFields(), [ - 'sort', - 'size', - ]); - if (searchSourceFields.index) { - // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: - // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on Saved Object - // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` - const indexId = - typeof searchSourceFields.index === 'string' - ? searchSourceFields.index - : searchSourceFields.index.id; - const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - references.push({ - name: refName, - type: 'index-pattern', - id: indexId, - }); - searchSourceFields = { - ...searchSourceFields, - indexRefName: refName, - index: undefined, - }; - } - if (searchSourceFields.filter) { - searchSourceFields = { - ...searchSourceFields, - filter: searchSourceFields.filter.map((filterRow: any, i: number) => { - if (!filterRow.meta || !filterRow.meta.index) { - return filterRow; - } - const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - references.push({ - name: refName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - return { - ...filterRow, - meta: { - ...filterRow.meta, - indexRefName: refName, - index: undefined, - }, - }; - }), - }; - } - attributes.kibanaSavedObjectMeta = { - searchSourceJSON: angular.toJson(searchSourceFields), - }; + const { + searchSourceJSON, + references: searchSourceReferences, + } = savedObject.searchSource.serialize(); + attributes.kibanaSavedObjectMeta = { searchSourceJSON }; + references.push(...searchSourceReferences); + } + + if (savedObject.unresolvedIndexPatternReference) { + references.push(savedObject.unresolvedIndexPatternReference); } return { attributes, references }; diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts index 08389e9e3c97f..60c66f84080b2 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts @@ -103,9 +103,11 @@ describe('Saved Object', () => { } beforeEach(() => { + (dataStartMock.search.createSearchSource as jest.Mock).mockReset(); SavedObjectClass = createSavedObjectClass({ savedObjectsClient: savedObjectsClientStub, indexPatterns: dataStartMock.indexPatterns, + search: dataStartMock.search, } as SavedObjectKibanaServices); }); @@ -269,7 +271,7 @@ describe('Saved Object', () => { ); }); - it('when index exists in searchSourceJSON', () => { + it('when search source references saved object', () => { const id = '123'; stubESResponse(getMockedDocResponse(id)); return createInitializedSavedObject({ type: 'dashboard', searchSource: true }).then( @@ -409,18 +411,17 @@ describe('Saved Object', () => { }); }); - it('throws error invalid JSON is detected', async () => { + it('forwards thrown exceptions from createSearchSource', async () => { + (dataStartMock.search.createSearchSource as jest.Mock).mockImplementation(() => { + throw new InvalidJSONProperty(''); + }); const savedObject = await createInitializedSavedObject({ type: 'dashboard', searchSource: true, }); const response = { found: true, - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON: '"{\\n \\"filter\\": []\\n}"', - }, - }, + _source: {}, }; try { @@ -586,23 +587,24 @@ describe('Saved Object', () => { }); }); - it('injects references from searchSourceJSON', async () => { + it('passes references to search source parsing function', async () => { const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true }); return savedObject.init!().then(() => { + const searchSourceJSON = JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + filter: [ + { + meta: { + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + }, + ], + }); const response = { found: true, _source: { kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', - filter: [ - { - meta: { - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - }, - }, - ], - }), + searchSourceJSON, }, }, references: [ @@ -619,16 +621,10 @@ describe('Saved Object', () => { ], }; savedObject.applyESResp(response); - expect(savedObject.searchSource!.getFields()).toEqual({ - index: 'my-index-1', - filter: [ - { - meta: { - index: 'my-index-2', - }, - }, - ], - }); + expect(dataStartMock.search.createSearchSource).toBeCalledWith( + searchSourceJSON, + response.references + ); }); }); }); diff --git a/src/plugins/saved_objects/public/types.ts b/src/plugins/saved_objects/public/types.ts index 99088df84ec36..3184038040952 100644 --- a/src/plugins/saved_objects/public/types.ts +++ b/src/plugins/saved_objects/public/types.ts @@ -24,7 +24,12 @@ import { SavedObjectAttributes, SavedObjectReference, } from 'kibana/public'; -import { IIndexPattern, IndexPatternsContract, ISearchSource } from '../../data/public'; +import { + DataPublicPluginStart, + IIndexPattern, + IndexPatternsContract, + ISearchSource, +} from '../../data/public'; export interface SavedObject { _serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] }; @@ -49,6 +54,7 @@ export interface SavedObject { searchSource?: ISearchSource; showInRecentlyAccessed: boolean; title: string; + unresolvedIndexPatternReference?: SavedObjectReference; } export interface SavedObjectSaveOpts { @@ -65,6 +71,7 @@ export interface SavedObjectCreationOpts { export interface SavedObjectKibanaServices { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; + search: DataPublicPluginStart['search']; chrome: ChromeStart; overlays: OverlayStart; } @@ -72,7 +79,6 @@ export interface SavedObjectKibanaServices { export interface SavedObjectConfig { // is only used by visualize afterESResp?: (savedObject: SavedObject) => Promise; - clearSavedIndexPattern?: boolean; defaults?: any; extractReferences?: (opts: { attributes: SavedObjectAttributes; diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index 2b77b6f4592a8..2b1159be89003 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -166,7 +166,7 @@ export class UrlPanelContent extends Component { // Get the application route, after the hash, and remove the #. const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); - return formatUrl({ + let formattedUrl = formatUrl({ protocol: parsedUrl.protocol, auth: parsedUrl.auth, host: parsedUrl.host, @@ -180,6 +180,11 @@ export class UrlPanelContent extends Component { }, }), }); + if (this.props.isEmbedded) { + formattedUrl = this.makeUrlEmbeddable(url); + } + + return formattedUrl; }; private getSnapshotUrl = () => { diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 2b2fc004a84c6..f532c2c8aa219 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiComponent } from 'src/plugins/kibana_utils/common'; +import { UiComponent } from 'src/plugins/kibana_utils/public'; import { ActionType, ActionContextMapping } from '../types'; export type ActionByType = Action; diff --git a/src/plugins/ui_actions/public/actions/action_definition.ts b/src/plugins/ui_actions/public/actions/action_definition.ts index c590cf8f34ee0..3eaa13572a826 100644 --- a/src/plugins/ui_actions/public/actions/action_definition.ts +++ b/src/plugins/ui_actions/public/actions/action_definition.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiComponent } from 'src/plugins/kibana_utils/common'; +import { UiComponent } from 'src/plugins/kibana_utils/public'; import { ActionType, ActionContextMapping } from '../types'; export interface ActionDefinition { diff --git a/src/plugins/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss index 985381eeb56a5..2187baea547da 100644 --- a/src/plugins/vis_default_editor/public/_default.scss +++ b/src/plugins/vis_default_editor/public/_default.scss @@ -72,6 +72,7 @@ display: flex; flex-basis: 100%; flex: 1; + overflow: hidden; @include euiBreakpoint('xs', 's', 'm') { // If we are on a small screen we force the visualization to take 100% width. diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 18af94c919247..f3192ba3da81f 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -94,8 +94,11 @@ const getSchemas = ( const createSchemaConfig = (accessor: number, agg: IAggConfig): SchemaConfig => { if (isDateHistogramBucketAggConfig(agg)) { agg.params.timeRange = timeRange; - const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; - agg.buckets.setBounds(agg.fieldIsTimeField() && bounds); + const bounds = + agg.params.timeRange && agg.fieldIsTimeField() + ? timefilter.calculateBounds(agg.params.timeRange) + : undefined; + agg.buckets.setBounds(bounds); agg.buckets.setInterval(agg.params.interval); } diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 216defcee9016..8fcb84b19a9be 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -26,6 +26,7 @@ import { setCapabilities, setHttp, setIndexPatterns, + setSearch, setSavedObjects, setUsageCollector, setFilterManager, @@ -140,6 +141,7 @@ export class VisualizationsPlugin setHttp(core.http); setSavedObjects(core.savedObjects); setIndexPatterns(data.indexPatterns); + setSearch(data.search); setFilterManager(data.query.filterManager); setExpressions(expressions); setUiActions(uiActions); @@ -150,6 +152,7 @@ export class VisualizationsPlugin const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, + search: data.search, chrome: core.chrome, overlays: core.overlays, visualizationTypes: types, diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index bc96e08f4b9da..c99c7a4c2caa1 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -35,7 +35,7 @@ import { extractReferences, injectReferences } from './saved_visualization_refer import { IIndexPattern, ISearchSource, SearchSource } from '../../../../plugins/data/public'; import { ISavedVis, SerializedVis } from '../types'; import { createSavedSearchesLoader } from '../../../../plugins/discover/public'; -import { getChrome, getOverlays, getIndexPatterns, getSavedObjects } from '../services'; +import { getChrome, getOverlays, getIndexPatterns, getSavedObjects, getSearch } from '../services'; export const convertToSerializedVis = async (savedVis: ISavedVis): Promise => { const { visState } = savedVis; @@ -87,6 +87,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: const savedSearch = await createSavedSearchesLoader({ savedObjectsClient: getSavedObjects().client, indexPatterns: getIndexPatterns(), + search: getSearch(), chrome: getChrome(), overlays: getOverlays(), }).get(savedSearchId); diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index c4668fa4b0c79..618c61dff176a 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -63,6 +63,8 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('Search'); + export const [getUsageCollector, setUsageCollector] = createGetterSetter( 'UsageCollection' ); diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts new file mode 100644 index 0000000000000..87a2da7e44a5e --- /dev/null +++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); + + describe('indexpattern without timefield', function() { + before(async function() { + await esArchiver.loadIfNeeded('index_pattern_without_timefield'); + }); + + beforeEach(async function() { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('without-timefield'); + }); + + after(async function unloadMakelogs() { + await esArchiver.unload('index_pattern_without_timefield'); + }); + + it('should not display a timepicker', async function() { + const timepickerExists = await PageObjects.timePicker.timePickerExists(); + expect(timepickerExists).to.be(false); + }); + + it('should display a timepicker after switching to an index pattern with timefield', async function() { + expect(await PageObjects.timePicker.timePickerExists()).to.be(false); + await PageObjects.discover.selectIndexPattern('with-timefield'); + expect(await PageObjects.timePicker.timePickerExists()).to.be(true); + }); + }); +} diff --git a/test/functional/apps/discover/index.js b/test/functional/apps/discover/index.js index 582c979a194f4..50f140b99aa1a 100644 --- a/test/functional/apps/discover/index.js +++ b/test/functional/apps/discover/index.js @@ -47,5 +47,6 @@ export default function({ getService, loadTestFile }) { loadTestFile(require.resolve('./_doc_navigation')); loadTestFile(require.resolve('./_date_nanos')); loadTestFile(require.resolve('./_date_nanos_mixed')); + loadTestFile(require.resolve('./_indexpattern_without_timefield')); }); } diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 53b7e7062ee2d..2f9d9f9bfb178 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -35,6 +35,7 @@ export default function({ getService, getPageObjects }) { afterEach(async function() { await esArchiver.unload('discover'); + await esArchiver.load('empty_kibana'); }); it('should import saved objects mgmt', async function() { diff --git a/test/functional/config.edge.js b/test/functional/config.edge.js new file mode 100644 index 0000000000000..ed68b41e8c89a --- /dev/null +++ b/test/functional/config.edge.js @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default async function({ readConfigFile }) { + const defaultConfig = await readConfigFile(require.resolve('./config')); + + return { + ...defaultConfig.getAll(), + + browser: { + type: 'msedge', + }, + + junit: { + reportName: 'MS Chromium Edge UI Functional Tests', + }, + }; +} diff --git a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json new file mode 100644 index 0000000000000..9493408a30040 --- /dev/null +++ b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json @@ -0,0 +1,65 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:without-timefield", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "without-timefield" + }, + "type": "index-pattern" + } + } +} + +{ + "type": "doc", + "value": { + "id": "AU_x3-TaGFA8no6QjiSJ", + "index": "without-timefield", + "source": { + "@message" : "5", + "@timestamp": "2019-09-22T23:50:13.253Z", + "referer": "http://twitter.com/error/takuya-onishi", + "request": "/uploads/dafydd-williams.jpg", + "response": "200", + "type": "apache", + "url": "https://media-for-the-masses.theacademyofperformingartsandscience.org/uploads/dafydd-williams.jpg" + } + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:with-timefield", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "with-timefield", + "timeFieldName": "@timestamp" + }, + "type": "index-pattern" + } + } +} + +{ + "type": "doc", + "value": { + "id": "AU_x3-TaGFA8no6QjiSJ", + "index": "with-timefield", + "source": { + "@message" : "5", + "@timestamp": "2019-09-22T23:50:13.253Z", + "referer": "http://twitter.com/error/takuya-onishi", + "request": "/uploads/dafydd-williams.jpg", + "response": "200", + "type": "apache", + "url": "https://media-for-the-masses.theacademyofperformingartsandscience.org/uploads/dafydd-williams.jpg" + } + } +} + diff --git a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json new file mode 100644 index 0000000000000..0096111923951 --- /dev/null +++ b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json @@ -0,0 +1,39 @@ +{ + "type": "index", + "value": { + "index": "without-timefield", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "index": "with-timefield", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 5017947e95d03..13d2365c07191 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -47,7 +47,9 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { */ public readonly browserType: string = browserType; - public readonly isChrome: boolean = browserType === Browsers.Chrome; + public readonly isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes( + browserType + ); public readonly isFirefox: boolean = browserType === Browsers.Firefox; diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 157918df874c8..8b57ecd3c8235 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -55,6 +55,7 @@ export class WebElementWrapper { private driver: WebDriver = this.webDriver.driver; private Keys = Key; public isW3CEnabled: boolean = (this.webDriver.driver as any).executor_.w3c === true; + public isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes(this.browserType); public static create( webElement: WebElement | WebElementWrapper, @@ -63,7 +64,7 @@ export class WebElementWrapper { timeout: number, fixedHeaderHeight: number, logger: ToolingLog, - browserType: string + browserType: Browsers ): WebElementWrapper { if (webElement instanceof WebElementWrapper) { return webElement; @@ -87,7 +88,7 @@ export class WebElementWrapper { private timeout: number, private fixedHeaderHeight: number, private logger: ToolingLog, - private browserType: string + private browserType: Browsers ) {} private async _findWithCustomTimeout( @@ -243,7 +244,7 @@ export class WebElementWrapper { return this.clearValueWithKeyboard(); } await this.retryCall(async function clearValue(wrapper) { - if (wrapper.browserType === Browsers.Chrome || options.withJS) { + if (wrapper.isChromium || options.withJS) { // https://bugs.chromium.org/p/chromedriver/issues/detail?id=2702 await wrapper.driver.executeScript(`arguments[0].value=''`, wrapper._webElement); } else { @@ -275,7 +276,7 @@ export class WebElementWrapper { await delay(100); } } else { - if (this.browserType === Browsers.Chrome) { + if (this.isChromium) { // https://bugs.chromium.org/p/chromedriver/issues/detail?id=30 await this.retryCall(async function clearValueWithKeyboard(wrapper) { await wrapper.driver.executeScript(`arguments[0].select();`, wrapper._webElement); diff --git a/test/functional/services/remote/browsers.ts b/test/functional/services/remote/browsers.ts index 46d81f1737a55..aa6e364d0a09d 100644 --- a/test/functional/services/remote/browsers.ts +++ b/test/functional/services/remote/browsers.ts @@ -21,4 +21,5 @@ export enum Browsers { Chrome = 'chrome', Firefox = 'firefox', InternetExplorer = 'ie', + ChromiumEdge = 'msedge', } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index e571a1a7e5551..b0724488cb5db 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -64,18 +64,23 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { lifecycle, config.get('browser.logPollingMs') ); + const isW3CEnabled = (driver as any).executor_.w3c; const caps = await driver.getCapabilities(); - const browserVersion = caps.get(isW3CEnabled ? 'browserVersion' : 'version'); + const browserVersion = caps.get( + isW3CEnabled || browserType === Browsers.ChromiumEdge ? 'browserVersion' : 'version' + ); - log.info(`Remote initialized: ${caps.get('browserName')} ${browserVersion}`); + log.info( + `Remote initialized: ${caps.get( + 'browserName' + )} ${browserVersion}, w3c compliance=${isW3CEnabled}, collectingCoverage=${collectCoverage}` + ); - if (browserType === Browsers.Chrome) { + if ([Browsers.Chrome, Browsers.ChromiumEdge].includes(browserType)) { log.info( - `Chromedriver version: ${ - caps.get('chrome').chromedriverVersion - }, w3c=${isW3CEnabled}, codeCoverage=${collectCoverage}` + `${browserType}driver version: ${caps.get(browserType)[`${browserType}driverVersion`]}` ); } diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 3bf5b865aa7ba..fc0b5bbb787c8 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -31,10 +31,12 @@ import { Builder, Capabilities, By, logging, until } from 'selenium-webdriver'; import chrome from 'selenium-webdriver/chrome'; import firefox from 'selenium-webdriver/firefox'; // @ts-ignore internal modules are not typed +import edge from 'selenium-webdriver/edge'; +import { installDriver } from 'ms-chromium-edge-driver'; +// @ts-ignore internal modules are not typed import { Executor } from 'selenium-webdriver/lib/http'; // @ts-ignore internal modules are not typed import { getLogger } from 'selenium-webdriver/lib/logging'; - import { pollForLogEntry$ } from './poll_for_log_entry'; import { createStdoutSocket } from './create_stdout_stream'; import { preventParallelCalls } from './prevent_parallel_calls'; @@ -63,6 +65,7 @@ Executor.prototype.execute = preventParallelCalls( ); let attemptCounter = 0; +let edgePaths: { driverPath: string | undefined; browserPath: string | undefined }; async function attemptToCreateCommand( log: ToolingLog, browserType: Browsers, @@ -74,6 +77,46 @@ async function attemptToCreateCommand( const buildDriverInstance = async () => { switch (browserType) { + case 'msedge': { + if (edgePaths && edgePaths.browserPath && edgePaths.driverPath) { + const edgeOptions = new edge.Options(); + if (headlessBrowser === '1') { + // @ts-ignore internal modules are not typed + edgeOptions.headless(); + } + // @ts-ignore internal modules are not typed + edgeOptions.setEdgeChromium(true); + // @ts-ignore internal modules are not typed + edgeOptions.setBinaryPath(edgePaths.browserPath); + const session = await new Builder() + .forBrowser('MicrosoftEdge') + .setEdgeOptions(edgeOptions) + .setEdgeService(new edge.ServiceBuilder(edgePaths.driverPath)) + .build(); + return { + session, + consoleLog$: pollForLogEntry$( + session, + logging.Type.BROWSER, + logPollingMs, + lifecycle.cleanup.after$ + ).pipe( + takeUntil(lifecycle.cleanup.after$), + map(({ message, level: { name: level } }) => ({ + message: message.replace(/\\n/g, '\n'), + level, + })) + ), + }; + } else { + throw new Error( + `Chromium Edge session requires browser or driver path to be defined: ${JSON.stringify( + edgePaths + )}` + ); + } + } + case 'chrome': { const chromeCapabilities = Capabilities.chrome(); const chromeOptions = [ @@ -265,6 +308,11 @@ export async function initWebDriver( log.verbose(entry.message); }); + // download Edge driver only in case of usage + if (browserType === Browsers.ChromiumEdge) { + edgePaths = await installDriver(); + } + return await Promise.race([ (async () => { await delay(2 * MINUTE); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index 205a303bcf47b..afa0cb51cd108 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -11,6 +11,12 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` "sortable": false, "width": "96px", }, + Object { + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + }, Object { "field": "message", "name": "Error message and culprit", @@ -142,7 +148,28 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` +
+ + Type + +
+ + List should render empty state 1`] = ` List should render empty state 1`] = ` aria-live="polite" aria-sort="descending" className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_occurrenceCount_3" + data-test-subj="tableHeaderCell_occurrenceCount_4" role="columnheader" scope="col" style={ @@ -225,7 +252,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` aria-live="polite" aria-sort="none" className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_latestOccurrenceAt_4" + data-test-subj="tableHeaderCell_latestOccurrenceAt_5" role="columnheader" scope="col" style={ @@ -264,7 +291,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > List should render with data 1`] = ` font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } +.c2 { + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .c1 { max-width: 100%; white-space: nowrap; @@ -301,7 +335,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` text-overflow: ellipsis; } -.c2 { +.c3 { font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; font-size: 16px; max-width: 100%; @@ -310,7 +344,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` text-overflow: ellipsis; } -.c3 { +.c4 { font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } @@ -324,6 +358,12 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` "sortable": false, "width": "96px", }, + Object { + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + }, Object { "field": "message", "name": "Error message and culprit", @@ -486,7 +526,28 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` +
+ + Type + +
+ + List should render with data 1`] = ` List should render with data 1`] = ` aria-live="polite" aria-sort="descending" className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_occurrenceCount_3" + data-test-subj="tableHeaderCell_occurrenceCount_4" role="columnheader" scope="col" style={ @@ -569,7 +630,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` aria-live="polite" aria-sort="none" className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_latestOccurrenceAt_4" + data-test-subj="tableHeaderCell_latestOccurrenceAt_5" role="columnheader" scope="col" style={ @@ -642,6 +703,49 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` + +
+ Type +
+ + List should render with data 1`] = ` className="" >
List should render with data 1`] = ` serviceName="opbeans-python" > List should render with data 1`] = ` onFocus={[Function]} >
@@ -812,6 +916,49 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
+ +
+ Type +
+
+ List should render with data 1`] = ` className="" >
List should render with data 1`] = ` serviceName="opbeans-python" > List should render with data 1`] = ` onFocus={[Function]} >
@@ -982,6 +1129,49 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
+ +
+ Type +
+
+ List should render with data 1`] = ` className="" >
List should render with data 1`] = ` serviceName="opbeans-python" > List should render with data 1`] = ` onFocus={[Function]} >
@@ -1152,6 +1342,49 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
+ +
+ Type +
+
+ List should render with data 1`] = ` className="" >
List should render with data 1`] = ` serviceName="opbeans-python" > List should render with data 1`] = ` onFocus={[Function]} >
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index b26833c02fe22..250b9a5d188d0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -23,6 +23,8 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ManagedTable } from '../../../shared/ManagedTable'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { APMQueryParams } from '../../../shared/Links/url_helpers'; const GroupIdLink = styled(ErrorDetailLink)` font-family: ${fontFamilyCode}; @@ -32,6 +34,10 @@ const MessageAndCulpritCell = styled.div` ${truncate('100%')}; `; +const ErrorLink = styled(ErrorOverviewLink)` + ${truncate('100%')}; +`; + const MessageLink = styled(ErrorDetailLink)` font-family: ${fontFamilyCode}; font-size: ${fontSizes.large}; @@ -48,9 +54,8 @@ interface Props { const ErrorGroupList: React.FC = props => { const { items } = props; - const { - urlParams: { serviceName } - } = useUrlParams(); + const { urlParams } = useUrlParams(); + const { serviceName } = urlParams; if (!serviceName) { throw new Error('Service name is required'); @@ -73,6 +78,29 @@ const ErrorGroupList: React.FC = props => { ); } }, + { + name: i18n.translate('xpack.apm.errorsTable.typeColumnLabel', { + defaultMessage: 'Type' + }), + field: 'type', + sortable: false, + render: (type: string, item: ErrorGroupListAPIResponse[0]) => { + return ( + + {type} + + ); + } + }, { name: i18n.translate( 'xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel', @@ -150,7 +178,7 @@ const ErrorGroupList: React.FC = props => { ) } ], - [serviceName] + [serviceName, urlParams] ); return ( diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 21bedc204f48b..e4b656ae8160d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -14,8 +14,6 @@ import React, { useState } from 'react'; import { debounce } from 'lodash'; -import { isRumAgentName } from '../../../../../../../plugins/apm/common/agent_name'; -import { AGENT_NAME } from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; import { animationOptions, cytoscapeOptions, @@ -96,10 +94,15 @@ function getLayoutOptions( } function selectRoots(cy: cytoscape.Core): string[] { - const nodes = cy.nodes(); - const roots = nodes.roots(); - const rumNodes = nodes.filter(node => isRumAgentName(node.data(AGENT_NAME))); - return rumNodes.union(roots).map(node => node.id()); + const bfs = cy.elements().bfs({ + roots: cy.elements().leaves() + }); + const furthestNodeFromLeaves = bfs.path.last(); + return cy + .elements() + .roots() + .union(furthestNodeFromLeaves) + .map(el => el.id()); } export function Cytoscape({ @@ -168,15 +171,26 @@ export function Cytoscape({ layout.run(); } }; + let layoutstopDelayTimeout: NodeJS.Timeout; const layoutstopHandler: cytoscape.EventHandler = event => { - event.cy.animate({ - ...animationOptions, - center: { - eles: serviceName - ? event.cy.getElementById(serviceName) - : event.cy.collection() + // This 0ms timer is necessary to prevent a race condition + // between the layout finishing rendering and viewport centering + layoutstopDelayTimeout = setTimeout(() => { + if (serviceName) { + event.cy.animate({ + ...animationOptions, + fit: { + eles: event.cy.elements(), + padding: nodeHeight + }, + center: { + eles: event.cy.getElementById(serviceName) + } + }); + } else { + event.cy.fit(undefined, nodeHeight); } - }); + }, 0); }; // debounce hover tracking so it doesn't spam telemetry with redundant events const trackNodeEdgeHover = debounce( @@ -231,6 +245,7 @@ export function Cytoscape({ cy.removeListener('select', 'node', selectHandler); cy.removeListener('unselect', 'node', unselectHandler); } + clearTimeout(layoutstopDelayTimeout); }; }, [cy, height, serviceName, trackApmEvent, width]); diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 905c88a6d18a0..532c49803e7b0 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -27,7 +27,6 @@ import 'uiExports/search'; import 'uiExports/shareContextMenuExtensions'; import _ from 'lodash'; import 'ui/autoload/all'; -import 'ui/agg_response'; import 'leaflet'; import { npStart } from 'ui/new_platform'; import { localApplicationService } from 'plugins/kibana/local_application_service'; diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index 252d602e8f564..bc636c0b200f8 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -17,6 +17,7 @@ module.service('gisMapSavedObjectLoader', function() { const services = { savedObjectsClient, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss index 9ebaee57fba4d..e319535b4a45c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss @@ -4,3 +4,7 @@ pointer-events: all; padding-left: $euiSizeM; } + +.mapAttributionControl__fullScreen { + margin-left: $euiSizeXXL * 4; +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js index e73a51ffa2ced..8bad536b39245 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js @@ -7,10 +7,12 @@ import { connect } from 'react-redux'; import { AttributionControl } from './view'; import { getLayerList } from '../../../selectors/map_selectors'; +import { getIsFullScreen } from '../../../selectors/ui_selectors'; function mapStateToProps(state = {}) { return { layerList: getLayerList(state), + isFullScreen: getIsFullScreen(state), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js index 161b5b81c1255..8f11d1b23376c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js @@ -7,6 +7,7 @@ import React, { Fragment } from 'react'; import _ from 'lodash'; import { EuiText, EuiLink } from '@elastic/eui'; +import classNames from 'classnames'; export class AttributionControl extends React.Component { state = { @@ -86,7 +87,11 @@ export class AttributionControl extends React.Component { return null; } return ( -
+
{this._renderAttributions()} diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap index 94d951a94fe29..cb1081c0c14da 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap @@ -25,6 +25,7 @@ exports[`Step1 editing should allow for editing 1`] = ` "actionTypeId": "1abc", "config": Object {}, "id": "1", + "isPreconfigured": false, "name": "Testing", } } diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx index 0933cd22db7c9..eaa474ba177b1 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx @@ -61,7 +61,7 @@ export const AlertsConfiguration: React.FC = ( async function fetchEmailActions() { const kibanaActions = await kfetch({ method: 'GET', - pathname: `/api/action/_find`, + pathname: `/api/action/_getAll`, }); const actions = kibanaActions.data.filter( diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx index 650294c29e9a5..19a1a61d00a42 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx @@ -27,6 +27,7 @@ describe('Step1', () => { actionTypeId: '1abc', name: 'Testing', config: {}, + isPreconfigured: false, }, ]; const selectedEmailActionId = emailActions[0].id; @@ -83,6 +84,7 @@ describe('Step1', () => { actionTypeId: '.email', name: '', config: {}, + isPreconfigured: false, }, ], selectedEmailActionId: NEW_ACTION_ID, diff --git a/x-pack/legacy/plugins/rollup/kibana.json b/x-pack/legacy/plugins/rollup/kibana.json index 3781d59d8c0f3..3df8bd7c187d5 100644 --- a/x-pack/legacy/plugins/rollup/kibana.json +++ b/x-pack/legacy/plugins/rollup/kibana.json @@ -4,7 +4,8 @@ "requiredPlugins": [ "home", "index_management", - "metrics" + "metrics", + "indexPatternManagement" ], "optionalPlugins": [ "usageCollection" diff --git a/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js b/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js index f0eb21a219442..f4de2a3098127 100644 --- a/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js +++ b/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { RollupPrompt } from './components/rollup_prompt'; -import { IndexPatternCreationConfig } from '../../../../../../src/legacy/core_plugins/management/public'; +import { IndexPatternCreationConfig } from '../../../../../../src/plugins/index_pattern_management/public'; const rollupIndexPatternTypeName = i18n.translate( 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultTypeName', diff --git a/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js b/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js index fbf2612b83aa8..809a76d1868b2 100644 --- a/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js +++ b/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternListConfig } from '../../../../../../src/legacy/core_plugins/management/public'; +import { IndexPatternListConfig } from '../../../../../../src/plugins/index_pattern_management/public'; function isRollup(indexPattern) { return ( diff --git a/x-pack/legacy/plugins/rollup/public/legacy.ts b/x-pack/legacy/plugins/rollup/public/legacy.ts index ec530e63408f4..83945110c2c76 100644 --- a/x-pack/legacy/plugins/rollup/public/legacy.ts +++ b/x-pack/legacy/plugins/rollup/public/legacy.ts @@ -6,14 +6,8 @@ import { npSetup, npStart } from 'ui/new_platform'; import { RollupPlugin } from './plugin'; -import { setup as management } from '../../../../../src/legacy/core_plugins/management/public/legacy'; const plugin = new RollupPlugin(); -export const setup = plugin.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - managementLegacy: management, - }, -}); +export const setup = plugin.setup(npSetup.core, npSetup.plugins); export const start = plugin.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/rollup/public/plugin.ts b/x-pack/legacy/plugins/rollup/public/plugin.ts index c58975419e20f..5782e88c3448b 100644 --- a/x-pack/legacy/plugins/rollup/public/plugin.ts +++ b/x-pack/legacy/plugins/rollup/public/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { PluginsStart } from './legacy_imports'; -import { ManagementSetup as ManagementSetupLegacy } from '../../../../../src/legacy/core_plugins/management/public/np_ready'; import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_management'; // @ts-ignore import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; @@ -26,6 +25,7 @@ import { import { CRUD_APP_BASE_PATH } from './crud_app/constants'; import { ManagementSetup } from '../../../../../src/plugins/management/public'; import { IndexMgmtSetup } from '../../../../plugins/index_management/public'; +import { IndexPatternManagementSetup } from '../../../../../src/plugins/index_pattern_management/public'; import { search } from '../../../../../src/plugins/data/public'; // @ts-ignore import { setEsBaseAndXPackBase, setHttp } from './crud_app/services'; @@ -33,23 +33,16 @@ import { setNotifications, setFatalErrors } from './kibana_services'; import { renderApp } from './application'; export interface RollupPluginSetupDependencies { - __LEGACY: { - managementLegacy: ManagementSetupLegacy; - }; home?: HomePublicPluginSetup; management: ManagementSetup; indexManagement?: IndexMgmtSetup; + indexPatternManagement: IndexPatternManagementSetup; } export class RollupPlugin implements Plugin { setup( core: CoreSetup, - { - __LEGACY: { managementLegacy }, - home, - management, - indexManagement, - }: RollupPluginSetupDependencies + { home, management, indexManagement, indexPatternManagement }: RollupPluginSetupDependencies ) { setFatalErrors(core.fatalErrors); @@ -61,8 +54,8 @@ export class RollupPlugin implements Plugin { const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); if (isRollupIndexPatternsEnabled) { - managementLegacy.indexPattern.creation.add(RollupIndexPatternCreationConfig); - managementLegacy.indexPattern.list.add(RollupIndexPatternListConfig); + indexPatternManagement.creation.addCreationConfig(RollupIndexPatternCreationConfig); + indexPatternManagement.list.addListConfig(RollupIndexPatternListConfig); } if (home) { diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 662fb8fb8ef68..22f1b3beffa35 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -65,8 +65,6 @@ export const INTERNAL_IDENTIFIER = '__internal'; export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id`; export const INTERNAL_RULE_ALERT_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_alert_id`; export const INTERNAL_IMMUTABLE_KEY = `${INTERNAL_IDENTIFIER}_immutable`; -export const INTERNAL_NOTIFICATION_ID_KEY = `${INTERNAL_IDENTIFIER}_notification_id`; -export const INTERNAL_NOTIFICATION_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_notification_rule_id`; /** * Detection engine routes diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts index ed47cdc62a1b6..c24081c777a96 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts @@ -6,7 +6,7 @@ import { isEmpty } from 'lodash/fp'; import { - CasesConnectorsFindResult, + Connector, CasesConfigurePatch, CasesConfigureResponse, CasesConfigureRequest, @@ -18,7 +18,7 @@ import { ApiProps } from '../types'; import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; import { CaseConfigure } from './types'; -export const fetchConnectors = async ({ signal }: ApiProps): Promise => { +export const fetchConnectors = async ({ signal }: ApiProps): Promise => { const response = await KibanaServices.get().http.fetch( `${CASES_CONFIGURE_URL}/connectors/_find`, { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx index d31dcdbee2a14..30108ecf33874 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx @@ -31,7 +31,7 @@ export const useConnectors = (): ReturnConnectors => { const res = await fetchConnectors({ signal: abortCtrl.signal }); if (!didCancel) { setLoading(false); - setConnectors(res.data); + setConnectors(res); } } catch (error) { if (!didCancel) { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx index a3df3664398ad..135f0f2a7e26d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx @@ -21,6 +21,7 @@ export const connectors: Connector[] = [ id: '123', actionTypeId: '.servicenow', name: 'My Connector', + isPreconfigured: false, config: { apiUrl: 'https://instance1.service-now.com', casesConfiguration: { @@ -48,6 +49,7 @@ export const connectors: Connector[] = [ id: '456', actionTypeId: '.servicenow', name: 'My Connector 2', + isPreconfigured: false, config: { apiUrl: 'https://instance2.service-now.com', casesConfiguration: { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts index 6955e57d099be..14b2e1ae9e366 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts @@ -6,5 +6,5 @@ import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; -export const addTags = (tags: string[] = [], ruleAlertId: string): string[] => +export const addTags = (tags: string[], ruleAlertId: string): string[] => Array.from(new Set([...tags, `${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}`])); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts index 073251b68f414..3878f5dae8889 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts @@ -24,7 +24,6 @@ describe('createNotifications', () => { enabled: true, interval: '', name: '', - tags: [], }); expect(alertsClient.create).toHaveBeenCalledWith( @@ -52,7 +51,6 @@ describe('createNotifications', () => { enabled: true, interval: '', name: '', - tags: [], }); expect(alertsClient.create).toHaveBeenCalledWith( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts index 3a1697f1c8afc..ccd7576255d83 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts @@ -17,12 +17,11 @@ export const createNotifications = async ({ ruleAlertId, interval, name, - tags, }: CreateNotificationParams): Promise => alertsClient.create({ data: { name, - tags: addTags(tags, ruleAlertId), + tags: addTags([], ruleAlertId), alertTypeId: NOTIFICATIONS_ID, consumer: APP_ID, params: { @@ -30,7 +29,7 @@ export const createNotifications = async ({ }, schedule: { interval }, enabled, - actions: actions?.map(transformRuleToAlertAction), + actions: actions.map(transformRuleToAlertAction), throttle: null, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index ced81098c9f8e..e4ad53de742d6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -45,7 +45,9 @@ export const rulesNotificationAlertType = ({ const ruleParams = { ...ruleAlertParams, name: ruleName, id: ruleAlertSavedObject.id }; const fromInMs = parseScheduleDates( - previousStartedAt ? previousStartedAt.toISOString() : `now-${ruleParams.interval}` + previousStartedAt + ? previousStartedAt.toISOString() + : `now-${ruleAlertSavedObject.attributes.schedule.interval}` )?.format('x'); const toInMs = parseScheduleDates(startedAt.toISOString())?.format('x'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts index 128a7965cd7dc..32a8737adc7c9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts @@ -45,10 +45,11 @@ export interface Clients { alertsClient: AlertsClient; } -export type UpdateNotificationParams = Omit & { +export type UpdateNotificationParams = Omit< + NotificationAlertParams, + 'interval' | 'actions' | 'tags' +> & { actions: RuleAlertAction[]; - id?: string; - tags?: string[]; interval: string | null | undefined; ruleAlertId: string; } & Clients; @@ -64,8 +65,6 @@ export interface NotificationAlertParams { ruleAlertId: string; interval: string; name: string; - tags?: string[]; - throttle?: null; } export type CreateNotificationParams = NotificationAlertParams & Clients; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts index 4c077dd9fc1fb..e1f7526438c31 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts @@ -9,6 +9,7 @@ import { updateNotifications } from './update_notifications'; import { readNotifications } from './read_notifications'; import { createNotifications } from './create_notifications'; import { getNotificationResult } from '../routes/__mocks__/request_responses'; +import { UpdateNotificationParams } from './types'; jest.mock('./read_notifications'); jest.mock('./create_notifications'); @@ -30,7 +31,6 @@ describe('updateNotifications', () => { enabled: true, interval: '10m', name: '', - tags: [], }); expect(alertsClient.update).toHaveBeenCalledWith( @@ -48,14 +48,13 @@ describe('updateNotifications', () => { it('should create a new notification if did not exist', async () => { (readNotifications as jest.Mock).mockResolvedValue(null); - const params = { + const params: UpdateNotificationParams = { alertsClient, actions: [], ruleAlertId: 'new-rule-id', enabled: true, interval: '10m', name: '', - tags: [], }; await updateNotifications(params); @@ -73,7 +72,6 @@ describe('updateNotifications', () => { enabled: true, interval: null, name: '', - tags: [], }); expect(alertsClient.delete).toHaveBeenCalledWith( @@ -98,7 +96,6 @@ describe('updateNotifications', () => { enabled: true, interval: '10m', name: '', - tags: [], }); expect(alertsClient.update).toHaveBeenCalledWith( @@ -125,10 +122,8 @@ describe('updateNotifications', () => { alertsClient, actions: [], enabled: true, - id: notification.id, ruleAlertId, name: notification.name, - tags: notification.tags, interval: null, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts index 3197d21c0e95a..ac0de406aceb2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts @@ -15,50 +15,41 @@ export const updateNotifications = async ({ alertsClient, actions, enabled, - id, ruleAlertId, name, - tags, interval, }: UpdateNotificationParams): Promise => { - const notification = await readNotifications({ alertsClient, id, ruleAlertId }); + const notification = await readNotifications({ alertsClient, id: undefined, ruleAlertId }); if (interval && notification) { - const result = await alertsClient.update({ + return alertsClient.update({ id: notification.id, data: { - tags: addTags(tags, ruleAlertId), + tags: addTags([], ruleAlertId), name, schedule: { interval, }, - actions: actions?.map(transformRuleToAlertAction), + actions: actions.map(transformRuleToAlertAction), params: { ruleAlertId, }, throttle: null, }, }); - return result; - } - - if (interval && !notification) { - const result = await createNotifications({ + } else if (interval && !notification) { + return createNotifications({ alertsClient, enabled, - tags, name, interval, actions, ruleAlertId, }); - return result; - } - - if (!interval && notification) { + } else if (!interval && notification) { await alertsClient.delete({ id: notification.id }); return null; + } else { + return null; } - - return null; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 77c1641d073c6..e400360a5a5b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -383,6 +383,7 @@ export const createActionResult = (): ActionResult => ({ actionTypeId: 'action-id-1', name: '', config: {}, + isPreconfigured: false, }); export const nonRuleAlert = () => ({ @@ -518,6 +519,7 @@ export const updateActionResult = (): ActionResult => ({ actionTypeId: 'action-id-1', name: '', config: {}, + isPreconfigured: false, }); export const getMockPrivilegesResult = () => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index d0e36515946a8..5377e9039785e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -144,6 +144,7 @@ export const createRulesBulkRoute = (router: IRouter) => { note, version, lists, + actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is set to rule, otherwise we are a notification and should not enable it, }); const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 6038ad2095323..9a329b78b8f12 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -132,6 +132,7 @@ export const createRulesRoute = (router: IRouter): void => { note, version: 1, lists, + actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it, }); const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 43e970702ba72..29ae5056a3ae8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -196,6 +196,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config note, version, lists, + actions: [], // Actions are not imported nor exported at this time }); resolve({ rule_id: ruleId, status_code: 200 }); } else if (rule != null && request.query.overwrite) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 9916972f41843..36e15780f5cb3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -122,6 +122,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { note, version, lists, + actions, }); if (rule != null) { const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 21dd2a4429cca..0444c757a9b31 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -118,6 +118,7 @@ export const updateRulesRoute = (router: IRouter) => { note, version, lists, + actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it }); if (rule != null) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts index 97cfc1d2d9ea7..991690d901d8a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts @@ -9,6 +9,7 @@ import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; import { getThrottleOptions, getRuleActionsFromSavedObject } from './utils'; +import { RulesActionsSavedObject } from './get_rule_actions_saved_object'; interface CreateRuleActionsSavedObject { ruleAlertId: string; @@ -22,12 +23,7 @@ export const createRuleActionsSavedObject = async ({ savedObjectsClient, actions = [], throttle, -}: CreateRuleActionsSavedObject): Promise<{ - id: string; - actions: RuleAlertAction[]; - alertThrottle: string | null; - ruleThrottle: string; -}> => { +}: CreateRuleActionsSavedObject): Promise => { const ruleActionsSavedObject = await savedObjectsClient.create< IRuleActionsAttributesSavedObjectAttributes >(ruleActionsSavedObjectType, { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts index 864281da5bafd..91489334940bd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts @@ -18,8 +18,9 @@ export const deleteRuleActionsSavedObject = async ({ savedObjectsClient, }: DeleteRuleActionsSavedObject): Promise<{} | null> => { const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); - - if (!ruleActions) return null; - - return savedObjectsClient.delete(ruleActionsSavedObjectType, ruleActions.id); + if (ruleActions != null) { + return savedObjectsClient.delete(ruleActionsSavedObjectType, ruleActions.id); + } else { + return null; + } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts index 61b544db5a18a..dad35f6cb1f96 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts @@ -15,15 +15,17 @@ interface GetRuleActionsSavedObject { savedObjectsClient: AlertServices['savedObjectsClient']; } -export const getRuleActionsSavedObject = async ({ - ruleAlertId, - savedObjectsClient, -}: GetRuleActionsSavedObject): Promise<{ +export interface RulesActionsSavedObject { id: string; actions: RuleAlertAction[]; alertThrottle: string | null; ruleThrottle: string; -} | null> => { +} + +export const getRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, +}: GetRuleActionsSavedObject): Promise => { const { saved_objects } = await savedObjectsClient.find< IRuleActionsAttributesSavedObjectAttributes >({ @@ -35,7 +37,7 @@ export const getRuleActionsSavedObject = async ({ if (!saved_objects[0]) { return null; + } else { + return getRuleActionsFromSavedObject(saved_objects[0]); } - - return getRuleActionsFromSavedObject(saved_objects[0]); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts index adc87150f89a7..d79c61f6200e3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts @@ -24,16 +24,17 @@ export const updateOrCreateRuleActionsSavedObject = async ({ actions, throttle, }: UpdateOrCreateRuleActionsSavedObject): Promise => { - const currentRuleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); + const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); - if (currentRuleActions) { + if (ruleActions != null) { return updateRuleActionsSavedObject({ ruleAlertId, savedObjectsClient, actions, throttle, - }) as Promise; + ruleActions, + }); + } else { + return createRuleActionsSavedObject({ ruleAlertId, savedObjectsClient, actions, throttle }); } - - return createRuleActionsSavedObject({ ruleAlertId, savedObjectsClient, actions, throttle }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts index a15005110c56b..2a2c84838ed93 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts @@ -6,7 +6,7 @@ import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; -import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; +import { RulesActionsSavedObject } from './get_rule_actions_saved_object'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { getThrottleOptions } from './utils'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; @@ -16,6 +16,7 @@ interface DeleteRuleActionsSavedObject { savedObjectsClient: AlertServices['savedObjectsClient']; actions: RuleAlertAction[] | undefined; throttle: string | null | undefined; + ruleActions: RulesActionsSavedObject; } export const updateRuleActionsSavedObject = async ({ @@ -23,16 +24,8 @@ export const updateRuleActionsSavedObject = async ({ savedObjectsClient, actions, throttle, -}: DeleteRuleActionsSavedObject): Promise<{ - ruleThrottle: string; - alertThrottle: string | null; - actions: RuleAlertAction[]; - id: string; -} | null> => { - const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); - - if (!ruleActions) return null; - + ruleActions, +}: DeleteRuleActionsSavedObject): Promise => { const throttleOptions = throttle ? getThrottleOptions(throttle) : { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts index 4c8d0f51f251b..a60f1d4177978 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts @@ -34,6 +34,7 @@ describe('createRules', () => { interval: '', name: '', tags: [], + actions: [], }); expect(alertsClient.create).toHaveBeenCalledWith( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index bebf4f350483b..91effb4741b8b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { Alert } from '../../../../../../../plugins/alerting/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; @@ -42,6 +43,7 @@ export const createRules = async ({ note, version, lists, + actions, }: CreateRuleParams): Promise => { // TODO: Remove this and use regular lists once the feature is stable for a release const listsParam = hasListsFeature() ? { lists } : {}; @@ -81,7 +83,7 @@ export const createRules = async ({ }, schedule: { interval }, enabled, - actions: [], + actions: actions.map(transformRuleToAlertAction), throttle: null, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index bcbe460fb6a66..6d4bacb9cc243 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -83,6 +83,7 @@ export const installPrepackagedRules = ( note, version, lists, + actions: [], // At this time there is no pre-packaged actions }), ]; }, []); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index d7655a15499eb..5c4889ec5fd68 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -120,7 +120,7 @@ export const patchRules = async ({ id: rule.id, data: { tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), - throttle: rule.throttle, + throttle: null, name: calculateName({ updatedName: name, originalName: rule.name }), schedule: { interval: calculateInterval(interval, rule.schedule.interval), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 38b1097a845f8..b1bed5d716155 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -142,12 +142,12 @@ export interface Clients { actionsClient: ActionsClient; } -export type PatchRuleParams = Partial> & { +export type PatchRuleParams = Partial> & { id: string | undefined | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; -export type UpdateRuleParams = Omit & { +export type UpdateRuleParams = Omit & { id: string | undefined | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; @@ -157,7 +157,7 @@ export type DeleteRuleParams = Clients & { ruleId: string | undefined | null; }; -export type CreateRuleParams = Omit & { +export type CreateRuleParams = Omit & { ruleId: string; } & Clients; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts deleted file mode 100644 index e6ee1e6a29764..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts +++ /dev/null @@ -1,54 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - AlertsClient, - AlertServices, - PartialAlert, -} from '../../../../../../../plugins/alerting/server'; -import { getRuleActionsSavedObject } from '../rule_actions/get_rule_actions_saved_object'; -import { readRules } from './read_rules'; -import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; - -interface UpdateRuleActions { - alertsClient: AlertsClient; - savedObjectsClient: AlertServices['savedObjectsClient']; - ruleAlertId: string; -} - -export const updateRuleActions = async ({ - alertsClient, - savedObjectsClient, - ruleAlertId, -}: UpdateRuleActions): Promise => { - const rule = await readRules({ alertsClient, id: ruleAlertId }); - if (rule == null) { - return null; - } - - const ruleActions = await getRuleActionsSavedObject({ - savedObjectsClient, - ruleAlertId, - }); - - if (!ruleActions) { - return null; - } - - return alertsClient.update({ - id: ruleAlertId, - data: { - actions: !ruleActions.alertThrottle - ? ruleActions.actions.map(transformRuleToAlertAction) - : [], - throttle: null, - name: rule.name, - tags: rule.tags, - schedule: rule.schedule, - params: rule.params, - }, - }); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts index ca299db6ace50..72f4cbcbe68e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts @@ -35,6 +35,7 @@ describe('updateRules', () => { interval: '', name: '', tags: [], + actions: [], }); expect(alertsClient.disable).toHaveBeenCalledWith( @@ -61,6 +62,7 @@ describe('updateRules', () => { interval: '', name: '', tags: [], + actions: [], }); expect(alertsClient.enable).toHaveBeenCalledWith( @@ -89,6 +91,7 @@ describe('updateRules', () => { interval: '', name: '', tags: [], + actions: [], }); expect(alertsClient.update).toHaveBeenCalledWith( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 0e70e05f4de78..99326768ed33b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../../../plugins/alerting/server'; import { readRules } from './read_rules'; import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; @@ -46,6 +47,7 @@ export const updateRules = async ({ lists, anomalyThreshold, machineLearningJobId, + actions, }: UpdateRuleParams): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -90,8 +92,8 @@ export const updateRules = async ({ tags: addTags(tags, rule.params.ruleId, rule.params.immutable), name, schedule: { interval }, - actions: rule.actions, - throttle: rule.throttle, + actions: actions.map(transformRuleToAlertAction), + throttle: null, params: { description, ruleId: rule.params.ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts index bb66a5ee1342f..994a54048b71a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -8,7 +8,6 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { AlertsClient, AlertServices } from '../../../../../../../plugins/alerting/server'; import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object'; import { updateNotifications } from '../notifications/update_notifications'; -import { updateRuleActions } from './update_rule_actions'; import { RuleActions } from '../rule_actions/types'; interface UpdateRulesNotifications { @@ -37,19 +36,13 @@ export const updateRulesNotifications = async ({ throttle, }); - await updateRuleActions({ - alertsClient, - savedObjectsClient, - ruleAlertId, - }); - await updateNotifications({ alertsClient, ruleAlertId, enabled, name, actions: ruleActions.actions, - interval: ruleActions?.alertThrottle, + interval: ruleActions.alertThrottle, }); return ruleActions; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh index 7804439ce0734..750c5574f4a72 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md#get-apiaction_find-find-actions curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_getAll \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 355041d9efbdb..ba8938f116fc6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -10,7 +10,7 @@ import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../../src/core/server'; import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { AnomalyResults, Anomaly } from '../../machine_learning'; @@ -29,6 +29,7 @@ interface BulkCreateMlSignalsParams { updatedBy: string; interval: string; enabled: boolean; + refresh: RefreshTypes; tags: string[]; throttle: string; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 414270ffcdd5c..81600b0b8dd9b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -52,6 +52,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -126,6 +127,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -156,6 +158,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -198,6 +201,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -240,6 +244,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -284,6 +289,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -328,6 +334,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -374,6 +381,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index ff81730bc4a72..3a964cb91fbdb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -6,7 +6,7 @@ import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { Logger } from '../../../../../../../../src/core/server'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; @@ -30,6 +30,7 @@ interface SearchAfterAndBulkCreateParams { enabled: boolean; pageSize: number; filter: unknown; + refresh: RefreshTypes; tags: string[]; throttle: string; } @@ -61,6 +62,7 @@ export const searchAfterAndBulkCreate = async ({ interval, enabled, pageSize, + refresh, tags, throttle, }: SearchAfterAndBulkCreateParams): Promise => { @@ -92,6 +94,7 @@ export const searchAfterAndBulkCreate = async ({ updatedBy, interval, enabled, + refresh, tags, throttle, }); @@ -179,6 +182,7 @@ export const searchAfterAndBulkCreate = async ({ updatedBy, interval, enabled, + refresh, tags, throttle, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 3d6f443ce60d6..03fb5832fdf42 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -105,6 +105,7 @@ describe('rules_notification_alert_type', () => { }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); + (searchAfterAndBulkCreate as jest.Mock).mockClear(); (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ success: true, searchAfterTimes: [], @@ -149,6 +150,37 @@ describe('rules_notification_alert_type', () => { }); }); + it("should set refresh to 'wait_for' when actions are present", async () => { + const ruleAlert = getResult(); + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + await alert.executor(payload); + expect((searchAfterAndBulkCreate as jest.Mock).mock.calls[0][0].refresh).toEqual('wait_for'); + (searchAfterAndBulkCreate as jest.Mock).mockClear(); + }); + + it('should set refresh to false when actions are not present', async () => { + await alert.executor(payload); + expect((searchAfterAndBulkCreate as jest.Mock).mock.calls[0][0].refresh).toEqual(false); + (searchAfterAndBulkCreate as jest.Mock).mockClear(); + }); + it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => { const ruleAlert = getResult(); ruleAlert.actions = [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index faac4a547fc17..0357f906f8035 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -98,6 +98,7 @@ export const signalRulesAlertType = ({ params: ruleParams, } = savedObject.attributes; const updatedAt = savedObject.updated_at ?? ''; + const refresh = actions.length ? 'wait_for' : false; const buildRuleMessage = buildRuleMessageFactory({ id: alertId, ruleId, @@ -181,6 +182,7 @@ export const signalRulesAlertType = ({ updatedAt, interval, enabled, + refresh, tags, }); result.success = success; @@ -241,6 +243,7 @@ export const signalRulesAlertType = ({ interval, enabled, pageSize: searchAfterSize, + refresh, tags, throttle, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 56f061cdfa3ca..45365b446cbf0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -159,6 +159,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -192,6 +193,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -217,6 +219,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -243,6 +246,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -271,6 +275,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -365,6 +370,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts index 6dd8823b57e4d..fc33d0e15e43f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -9,7 +9,7 @@ import { performance } from 'perf_hooks'; import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { SignalSearchResponse, BulkResponse } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { generateId, makeFloatString } from './utils'; import { buildBulkBody } from './build_bulk_body'; import { Logger } from '../../../../../../../../src/core/server'; @@ -31,6 +31,7 @@ interface SingleBulkCreateParams { enabled: boolean; tags: string[]; throttle: string; + refresh: RefreshTypes; } /** @@ -77,6 +78,7 @@ export const singleBulkCreate = async ({ updatedBy, interval, enabled, + refresh, tags, throttle, }: SingleBulkCreateParams): Promise => { @@ -124,7 +126,7 @@ export const singleBulkCreate = async ({ const start = performance.now(); const response: BulkResponse = await services.callCluster('bulk', { index: signalsIndex, - refresh: false, + refresh, body: bulkBody, }); const end = performance.now(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index d4469351de544..040e32aa0d360 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -162,5 +162,10 @@ export interface AlertAttributes { } export interface RuleAlertAttributes extends AlertAttributes { - params: Omit & { ruleId: string }; + params: Omit< + RuleAlertParams, + 'ruleId' | 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' + > & { + ruleId: string; + }; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index d3fa98fd73d3a..035f1b10ff8b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -149,3 +149,5 @@ export type CallWithRequest, V> = ( params: T, options?: CallAPIOptions ) => Promise; + +export type RefreshTypes = false | 'wait_for'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx index b547f8b076f93..a49468ad3dd06 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx @@ -17,7 +17,7 @@ interface Props { export const UptimeAlertsFlyoutWrapper = ({ alertTypeId, canChangeTrigger }: Props) => { const dispatch = useDispatch(); - const setAddFlyoutVisiblity = (value: React.SetStateAction) => + const setAddFlyoutVisibility = (value: React.SetStateAction) => // @ts-ignore the value here is a boolean, and it works with the action creator function dispatch(setAlertFlyoutVisible(value)); @@ -28,7 +28,7 @@ export const UptimeAlertsFlyoutWrapper = ({ alertTypeId, canChangeTrigger }: Pro alertFlyoutVisible={alertFlyoutVisible} alertTypeId={alertTypeId} canChangeTrigger={canChangeTrigger} - setAlertFlyoutVisibility={setAddFlyoutVisiblity} + setAlertFlyoutVisibility={setAddFlyoutVisibility} /> ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx index 5143e1c963904..b86e85f35b17d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx @@ -268,7 +268,21 @@ export const AlertMonitorStatusComponent: React.FC = pr /> } data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression" - description="any monitor is down >" + description={ + filters + ? i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesExpression.matchingMonitors.description', + { + defaultMessage: 'matching monitors are down >', + } + ) + : i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesExpression.anyMonitors.description', + { + defaultMessage: 'any monitor is down >', + } + ) + } id="ping-count" value={`${numTimes} times`} /> diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index d217d26e84836..82cc09f5e9eca 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -28,7 +28,7 @@ Table of Contents - [RESTful API](#restful-api) - [`POST /api/action`: Create action](#post-apiaction-create-action) - [`DELETE /api/action/{id}`: Delete action](#delete-apiactionid-delete-action) - - [`GET /api/action/_find`: Find actions](#get-apiactionfind-find-actions) + - [`GET /api/action/_getAll`: Get all actions](#get-apiaction-get-all-actions) - [`GET /api/action/{id}`: Get action](#get-apiactionid-get-action) - [`GET /api/action/types`: List action types](#get-apiactiontypes-list-action-types) - [`PUT /api/action/{id}`: Update action](#put-apiactionid-update-action) @@ -92,6 +92,7 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba | _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | | _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | | _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | +| _xpack.actions._**preconfigured** | A list of preconfigured actions. Default: `[]` | Array | #### Whitelisting Built-in Action Types @@ -174,11 +175,13 @@ Params: | -------- | --------------------------------------------- | ------ | | id | The id of the action you're trying to delete. | string | -### `GET /api/action/_find`: Find actions +### `GET /api/action/_getAll`: Get all actions -Params: +No parameters. -See the [saved objects API documentation for find](https://www.elastic.co/guide/en/kibana/master/saved-objects-api-find.html). All the properties are the same except that you cannot pass in `type`. +Return all actions from saved objects merged with predefined list. +Use the [saved objects API for find](https://www.elastic.co/guide/en/kibana/master/saved-objects-api-find.html) with the proprties: `type: 'action'` and `perPage: 10000`. +List of predefined actions should be set up in Kibana.yaml. ### `GET /api/action/{id}`: Get action diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index f3042a701211f..61b338d47b9f5 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -20,4 +20,5 @@ export interface ActionResult { actionTypeId: string; name: string; config: Record; + isPreconfigured: boolean; } diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 8a39d68f40bb6..431bfb1e99c3b 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -12,9 +12,9 @@ const createActionsClientMock = () => { const mocked: jest.Mocked = { create: jest.fn(), get: jest.fn(), - find: jest.fn(), delete: jest.fn(), update: jest.fn(), + getAll: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 0df07ad58fb9e..955e1569380a5 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -51,6 +51,7 @@ beforeEach(() => { savedObjectsClient, scopedClusterClient, defaultKibanaIndex, + preconfiguredActions: [], }); }); @@ -83,6 +84,7 @@ describe('create()', () => { }); expect(result).toEqual({ id: '1', + isPreconfigured: false, name: 'my name', actionTypeId: 'my-action-type', config: {}, @@ -178,6 +180,7 @@ describe('create()', () => { }); expect(result).toEqual({ id: '1', + isPreconfigured: false, name: 'my name', actionTypeId: 'my-action-type', config: { @@ -226,6 +229,7 @@ describe('create()', () => { savedObjectsClient, scopedClusterClient, defaultKibanaIndex, + preconfiguredActions: [], }); const savedObjectCreateResult = { @@ -305,6 +309,7 @@ describe('get()', () => { const result = await actionsClient.get({ id: '1' }); expect(result).toEqual({ id: '1', + isPreconfigured: false, }); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` @@ -314,9 +319,44 @@ describe('get()', () => { ] `); }); + + test('return predefined action with id', async () => { + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: { + test: 'test1', + }, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + + const result = await actionsClient.get({ id: 'testPreconfigured' }); + expect(result).toEqual({ + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + }); }); -describe('find()', () => { +describe('getAll()', () => { test('calls savedObjectsClient with parameters', async () => { const expectedResult = { total: 1, @@ -327,6 +367,7 @@ describe('find()', () => { id: '1', type: 'type', attributes: { + name: 'test', config: { foo: 'bar', }, @@ -339,31 +380,50 @@ describe('find()', () => { scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ aggregations: { '1': { doc_count: 6 }, + testPreconfigured: { doc_count: 2 }, }, }); - const result = await actionsClient.find({}); - expect(result).toEqual({ - total: 1, - perPage: 10, - page: 1, - data: [ + + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + preconfiguredActions: [ { - id: '1', + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + name: 'test', config: { foo: 'bar', }, - referencedByCount: 6, }, ], }); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "type": "action", + const result = await actionsClient.getAll(); + expect(result).toEqual([ + { + id: '1', + isPreconfigured: false, + name: 'test', + config: { + foo: 'bar', }, - ] - `); + referencedByCount: 6, + }, + { + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + referencedByCount: 2, + }, + ]); }); }); @@ -420,6 +480,7 @@ describe('update()', () => { }); expect(result).toEqual({ id: 'my-action', + isPreconfigured: false, actionTypeId: 'my-action-type', name: 'my name', config: {}, @@ -524,6 +585,7 @@ describe('update()', () => { }); expect(result).toEqual({ id: 'my-action', + isPreconfigured: false, actionTypeId: 'my-action-type', name: 'my name', config: { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 129829850f9c1..8f73bfb31ea4d 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -11,9 +11,16 @@ import { SavedObject, } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; import { ActionTypeRegistry } from './action_type_registry'; import { validateConfig, validateSecrets } from './lib'; -import { ActionResult, FindActionResult, RawAction } from './types'; +import { ActionResult, FindActionResult, RawAction, PreConfiguredAction } from './types'; +import { PreconfiguredActionDisabledModificationError } from './lib/errors/preconfigured_action_disabled_modification'; + +// We are assuming there won't be many actions. This is why we will load +// all the actions in advance and assume the total count to not go over 10000. +// We'll set this max setting assuming it's never reached. +export const MAX_ACTIONS_RETURNED = 10000; interface ActionUpdate extends SavedObjectAttributes { name: string; @@ -29,35 +36,12 @@ interface CreateOptions { action: Action; } -interface FindOptions { - options?: { - perPage?: number; - page?: number; - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - sortField?: string; - hasReference?: { - type: string; - id: string; - }; - fields?: string[]; - filter?: string; - }; -} - -interface FindResult { - page: number; - perPage: number; - total: number; - data: FindActionResult[]; -} - interface ConstructorOptions { defaultKibanaIndex: string; scopedClusterClient: IScopedClusterClient; actionTypeRegistry: ActionTypeRegistry; savedObjectsClient: SavedObjectsClientContract; + preconfiguredActions: PreConfiguredAction[]; } interface UpdateOptions { @@ -70,17 +54,20 @@ export class ActionsClient { private readonly scopedClusterClient: IScopedClusterClient; private readonly savedObjectsClient: SavedObjectsClientContract; private readonly actionTypeRegistry: ActionTypeRegistry; + private readonly preconfiguredActions: PreConfiguredAction[]; constructor({ actionTypeRegistry, defaultKibanaIndex, scopedClusterClient, savedObjectsClient, + preconfiguredActions, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; this.savedObjectsClient = savedObjectsClient; this.scopedClusterClient = scopedClusterClient; this.defaultKibanaIndex = defaultKibanaIndex; + this.preconfiguredActions = preconfiguredActions; } /** @@ -106,6 +93,7 @@ export class ActionsClient { actionTypeId: result.attributes.actionTypeId, name: result.attributes.name, config: result.attributes.config, + isPreconfigured: false, }; } @@ -113,6 +101,20 @@ export class ActionsClient { * Update action */ public async update({ id, action }: UpdateOptions): Promise { + if ( + this.preconfiguredActions.find(preconfiguredAction => preconfiguredAction.id === id) !== + undefined + ) { + throw new PreconfiguredActionDisabledModificationError( + i18n.translate('xpack.actions.serverSideErrors.predefinedActionUpdateDisabled', { + defaultMessage: 'Preconfigured action {id} is not allowed to update.', + values: { + id, + }, + }), + 'update' + ); + } const existingObject = await this.savedObjectsClient.get('action', id); const { actionTypeId } = existingObject.attributes; const { name, config, secrets } = action; @@ -134,6 +136,7 @@ export class ActionsClient { actionTypeId: result.attributes.actionTypeId as string, name: result.attributes.name as string, config: result.attributes.config as Record, + isPreconfigured: false, }; } @@ -141,6 +144,18 @@ export class ActionsClient { * Get an action */ public async get({ id }: { id: string }): Promise { + const preconfiguredActionsList = this.preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === id + ); + if (preconfiguredActionsList !== undefined) { + return { + id, + actionTypeId: preconfiguredActionsList.actionTypeId, + name: preconfiguredActionsList.name, + config: preconfiguredActionsList.config, + isPreconfigured: true, + }; + } const result = await this.savedObjectsClient.get('action', id); return { @@ -148,36 +163,56 @@ export class ActionsClient { actionTypeId: result.attributes.actionTypeId, name: result.attributes.name, config: result.attributes.config, + isPreconfigured: false, }; } /** - * Find actions + * Get all actions with preconfigured list */ - public async find({ options = {} }: FindOptions): Promise { - const findResult = await this.savedObjectsClient.find({ - ...options, - type: 'action', - }); + public async getAll(): Promise { + const savedObjectsActions = ( + await this.savedObjectsClient.find({ + perPage: MAX_ACTIONS_RETURNED, + type: 'action', + }) + ).saved_objects.map(actionFromSavedObject); - const data = await injectExtraFindData( + const mergedResult = [ + ...savedObjectsActions, + ...this.preconfiguredActions.map(preconfiguredAction => ({ + id: preconfiguredAction.id, + actionTypeId: preconfiguredAction.actionTypeId, + name: preconfiguredAction.name, + config: preconfiguredAction.config, + isPreconfigured: true, + })), + ].sort((a, b) => a.name.localeCompare(b.name)); + return await injectExtraFindData( this.defaultKibanaIndex, this.scopedClusterClient, - findResult.saved_objects.map(actionFromSavedObject) + mergedResult ); - - return { - page: findResult.page, - perPage: findResult.per_page, - total: findResult.total, - data, - }; } /** * Delete action */ public async delete({ id }: { id: string }) { + if ( + this.preconfiguredActions.find(preconfiguredAction => preconfiguredAction.id === id) !== + undefined + ) { + throw new PreconfiguredActionDisabledModificationError( + i18n.translate('xpack.actions.serverSideErrors.predefinedActionDeleteDisabled', { + defaultMessage: 'Preconfigured action {id} is not allowed to delete.', + values: { + id, + }, + }), + 'delete' + ); + } return await this.savedObjectsClient.delete('action', id); } } @@ -186,6 +221,7 @@ function actionFromSavedObject(savedObject: SavedObject): ActionResul return { id: savedObject.id, ...savedObject.attributes, + isPreconfigured: false, }; } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 67b7553c4a736..51e87dbd75b48 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -14,6 +14,44 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], + "preconfigured": Array [], + "whitelistedHosts": Array [ + "*", + ], + } + `); + }); + + test('action with preconfigured actions', () => { + const config: Record = { + preconfigured: [ + { + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack #xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }, + ], + }; + expect(configSchema.validate(config)).toMatchInlineSnapshot(` + Object { + "enabled": true, + "enabledActionTypes": Array [ + "*", + ], + "preconfigured": Array [ + Object { + "actionTypeId": ".slack", + "config": Object { + "webhookUrl": "https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz", + }, + "id": "my-slack1", + "name": "Slack #xyz", + "secrets": Object {}, + }, + ], "whitelistedHosts": Array [ "*", ], diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 9e4795be6c208..1f04efd1941b4 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -21,6 +21,18 @@ export const configSchema = schema.object({ defaultValue: [WhitelistedHosts.Any], } ), + preconfigured: schema.arrayOf( + schema.object({ + id: schema.string({ minLength: 1 }), + name: schema.string(), + actionTypeId: schema.string({ minLength: 1 }), + config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + }), + { + defaultValue: [], + } + ), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index eee2ae352fe3d..88553c314112f 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -11,7 +11,13 @@ import { ActionsClient as ActionsClientClass } from './actions_client'; export type ActionsClient = PublicMethodsOf; -export { ActionsPlugin, ActionResult, ActionTypeExecutorOptions, ActionType } from './types'; +export { + ActionsPlugin, + ActionResult, + ActionTypeExecutorOptions, + ActionType, + PreConfiguredAction, +} from './types'; export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initContext: PluginInitializerContext) => new ActionsPlugin(initContext); diff --git a/x-pack/plugins/actions/server/lib/errors/preconfigured_action_disabled_modification.ts b/x-pack/plugins/actions/server/lib/errors/preconfigured_action_disabled_modification.ts new file mode 100644 index 0000000000000..884353e132b9c --- /dev/null +++ b/x-pack/plugins/actions/server/lib/errors/preconfigured_action_disabled_modification.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaResponseFactory } from '../../../../../../src/core/server'; +import { ErrorThatHandlesItsOwnResponse } from './types'; + +export type PreconfiguredActionDisabledFrom = 'update' | 'delete'; + +export class PreconfiguredActionDisabledModificationError extends Error + implements ErrorThatHandlesItsOwnResponse { + public readonly disabledFrom: PreconfiguredActionDisabledFrom; + + constructor(message: string, disabledFrom: PreconfiguredActionDisabledFrom) { + super(message); + this.disabledFrom = disabledFrom; + } + + public sendResponse(res: KibanaResponseFactory) { + return res.badRequest({ body: { message: this.message } }); + } +} diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 75396f2aad897..bc4268bb69872 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -21,6 +21,7 @@ const createStartMock = () => { execute: jest.fn(), isActionTypeEnabled: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), + preconfiguredActions: [], }; return mock; }; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 383f84590fbc6..6215b08df81d4 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -31,7 +31,34 @@ describe('Actions Plugin', () => { let pluginsSetup: jest.Mocked; beforeEach(() => { - context = coreMock.createPluginInitializerContext(); + context = coreMock.createPluginInitializerContext({ + preconfigured: [ + { + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack #xyz', + description: 'Send a message to the #xyz channel', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }, + { + id: 'custom-system-abc-connector', + actionTypeId: 'system-abc-action-type', + description: 'Send a notification to system ABC', + name: 'System ABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + secrets: { + xyzSecret1: 'credential1', + xyzSecret2: 'credential2', + }, + }, + ], + }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -160,7 +187,9 @@ describe('Actions Plugin', () => { let pluginsStart: jest.Mocked; beforeEach(() => { - const context = coreMock.createPluginInitializerContext(); + const context = coreMock.createPluginInitializerContext({ + preconfigured: [], + }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index ce31e62bc9b8e..34c9e7aa9e8b8 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -30,7 +30,7 @@ import { LICENSE_TYPE } from '../../licensing/common/types'; import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; import { ActionsConfig } from './config'; -import { Services, ActionType } from './types'; +import { Services, ActionType, PreConfiguredAction } from './types'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; @@ -44,7 +44,7 @@ import { getActionsConfigurationUtilities } from './actions_config'; import { createActionRoute, deleteActionRoute, - findActionRoute, + getAllActionRoute, getActionRoute, updateActionRoute, listActionTypesRoute, @@ -67,6 +67,7 @@ export interface PluginStartContract { isActionTypeEnabled(id: string): boolean; execute(options: ExecuteOptions): Promise; getActionsClientWithRequest(request: KibanaRequest): Promise>; + preconfiguredActions: PreConfiguredAction[]; } export interface ActionsPluginsSetup { @@ -97,6 +98,7 @@ export class ActionsPlugin implements Plugin, Plugi private eventLogger?: IEventLogger; private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; + private readonly preconfiguredActions: PreConfiguredAction[]; constructor(initContext: PluginInitializerContext) { this.config = initContext.config @@ -113,6 +115,7 @@ export class ActionsPlugin implements Plugin, Plugi this.logger = initContext.logger.get('actions'); this.telemetryLogger = initContext.logger.get('telemetry'); + this.preconfiguredActions = []; } public async setup(core: CoreSetup, plugins: ActionsPluginsSetup): Promise { @@ -151,8 +154,14 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - const actionsConfigUtils = getActionsConfigurationUtilities( - (await this.config) as ActionsConfig + const actionsConfig = (await this.config) as ActionsConfig; + const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig); + + this.preconfiguredActions.push( + ...actionsConfig.preconfigured.map( + preconfiguredAction => + ({ ...preconfiguredAction, isPreconfigured: true } as PreConfiguredAction) + ) ); const actionTypeRegistry = new ActionTypeRegistry({ taskRunnerFactory, @@ -197,7 +206,7 @@ export class ActionsPlugin implements Plugin, Plugi createActionRoute(router, this.licenseState); deleteActionRoute(router, this.licenseState); getActionRoute(router, this.licenseState); - findActionRoute(router, this.licenseState); + getAllActionRoute(router, this.licenseState); updateActionRoute(router, this.licenseState); listActionTypesRoute(router, this.licenseState); executeActionRoute(router, this.licenseState, actionExecutor); @@ -226,6 +235,7 @@ export class ActionsPlugin implements Plugin, Plugi kibanaIndex, adminClient, isESOUsingEphemeralEncryptionKey, + preconfiguredActions, } = this; actionExecutor!.initialize({ @@ -271,8 +281,10 @@ export class ActionsPlugin implements Plugin, Plugi actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex: await kibanaIndex, scopedClusterClient: adminClient!.asScoped(request), + preconfiguredActions, }); }, + preconfiguredActions, }; } @@ -289,7 +301,12 @@ export class ActionsPlugin implements Plugin, Plugi private createRouteHandlerContext = ( defaultKibanaIndex: string ): IContextProvider, 'actions'> => { - const { actionTypeRegistry, adminClient, isESOUsingEphemeralEncryptionKey } = this; + const { + actionTypeRegistry, + adminClient, + isESOUsingEphemeralEncryptionKey, + preconfiguredActions, + } = this; return async function actionsRouteHandlerContext(context, request) { return { getActionsClient: () => { @@ -303,6 +320,7 @@ export class ActionsPlugin implements Plugin, Plugi actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex, scopedClusterClient: adminClient!.asScoped(request), + preconfiguredActions, }); }, listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!), diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index cddebb3a8e31e..ffd1f0faabbab 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -17,7 +17,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { ILicenseState, verifyApiAccess } from '../lib'; +import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; import { BASE_ACTION_API_PATH } from '../../common'; const paramSchema = schema.object({ @@ -46,8 +46,15 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState) } const actionsClient = context.actions.getActionsClient(); const { id } = req.params; - await actionsClient.delete({ id }); - return res.noContent(); + try { + await actionsClient.delete({ id }); + return res.noContent(); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/actions/server/routes/find.test.ts b/x-pack/plugins/actions/server/routes/find.test.ts deleted file mode 100644 index 1b130421fa71f..0000000000000 --- a/x-pack/plugins/actions/server/routes/find.test.ts +++ /dev/null @@ -1,152 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { findActionRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib'; -import { mockHandlerArguments } from './_mock_handler_arguments'; - -jest.mock('../lib/verify_api_access.ts', () => ({ - verifyApiAccess: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('findActionRoute', () => { - it('finds actions with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); - - findActionRoute(router, licenseState); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/action/_find"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); - - const findResult = { - page: 1, - perPage: 1, - total: 0, - data: [], - }; - const actionsClient = { - find: jest.fn().mockResolvedValueOnce(findResult), - }; - - const [context, req, res] = mockHandlerArguments( - { actionsClient }, - { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toMatchInlineSnapshot(` - Object { - "body": Object { - "data": Array [], - "page": 1, - "perPage": 1, - "total": 0, - }, - } - `); - - expect(actionsClient.find).toHaveBeenCalledTimes(1); - expect(actionsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "options": Object { - "defaultSearchOperator": "OR", - "fields": undefined, - "filter": undefined, - "page": 1, - "perPage": 1, - "search": undefined, - "sortField": undefined, - "sortOrder": undefined, - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: findResult, - }); - }); - - it('ensures the license allows finding actions', async () => { - const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); - - findActionRoute(router, licenseState); - - const [, handler] = router.get.mock.calls[0]; - - const actionsClient = { - find: jest.fn().mockResolvedValueOnce({ - page: 1, - perPage: 1, - total: 0, - data: [], - }), - }; - - const [context, req, res] = mockHandlerArguments(actionsClient, { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents finding actions', async () => { - const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - findActionRoute(router, licenseState); - - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - {}, - { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }, - ['ok'] - ); - expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); -}); diff --git a/x-pack/plugins/actions/server/routes/find.ts b/x-pack/plugins/actions/server/routes/find.ts deleted file mode 100644 index 45b967629a2a8..0000000000000 --- a/x-pack/plugins/actions/server/routes/find.ts +++ /dev/null @@ -1,95 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; -import { - IRouter, - RequestHandlerContext, - KibanaRequest, - IKibanaResponse, - KibanaResponseFactory, -} from 'kibana/server'; -import { FindOptions } from '../../../alerting/server'; -import { ILicenseState, verifyApiAccess } from '../lib'; -import { BASE_ACTION_API_PATH } from '../../common'; - -// config definition -const querySchema = schema.object({ - per_page: schema.number({ defaultValue: 20, min: 0 }), - page: schema.number({ defaultValue: 1, min: 1 }), - search: schema.maybe(schema.string()), - default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { - defaultValue: 'OR', - }), - search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), - sort_field: schema.maybe(schema.string()), - sort_order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), - has_reference: schema.maybe( - // use nullable as maybe is currently broken - // in config-schema - schema.nullable( - schema.object({ - type: schema.string(), - id: schema.string(), - }) - ) - ), - fields: schema.maybe(schema.arrayOf(schema.string())), - filter: schema.maybe(schema.string()), -}); - -export const findActionRoute = (router: IRouter, licenseState: ILicenseState) => { - router.get( - { - path: `${BASE_ACTION_API_PATH}/_find`, - validate: { - query: querySchema, - }, - options: { - tags: ['access:actions-read'], - }, - }, - router.handleLegacyErrors(async function( - context: RequestHandlerContext, - req: KibanaRequest, any, any>, - res: KibanaResponseFactory - ): Promise> { - verifyApiAccess(licenseState); - if (!context.actions) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); - } - const actionsClient = context.actions.getActionsClient(); - const query = req.query; - const options: FindOptions['options'] = { - perPage: query.per_page, - page: query.page, - search: query.search, - defaultSearchOperator: query.default_search_operator, - sortField: query.sort_field, - fields: query.fields, - filter: query.filter, - sortOrder: query.sort_order, - }; - - if (query.search_fields) { - options.searchFields = Array.isArray(query.search_fields) - ? query.search_fields - : [query.search_fields]; - } - - if (query.has_reference) { - options.hasReference = query.has_reference; - } - - const findResult = await actionsClient.find({ - options, - }); - return res.ok({ - body: findResult, - }); - }) - ); -}; diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts new file mode 100644 index 0000000000000..6499427b8c1a5 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_all.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getAllActionRoute } from './get_all'; +import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib'; +import { mockHandlerArguments } from './_mock_handler_arguments'; + +jest.mock('../lib/verify_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getAllActionRoute', () => { + it('get all actions with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`); + expect(config.options).toMatchInlineSnapshot(` + Object { + "tags": Array [ + "access:actions-read", + ], + } + `); + + const actionsClient = { + getAll: jest.fn().mockResolvedValueOnce([]), + }; + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Array [], + } + `); + + expect(actionsClient.getAll).toHaveBeenCalledTimes(1); + + expect(res.ok).toHaveBeenCalledWith({ + body: [], + }); + }); + + it('ensures the license allows getting all actions', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`); + expect(config.options).toMatchInlineSnapshot(` + Object { + "tags": Array [ + "access:actions-read", + ], + } + `); + + const actionsClient = { + getAll: jest.fn().mockResolvedValueOnce([]), + }; + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents getting all actions', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`); + expect(config.options).toMatchInlineSnapshot(` + Object { + "tags": Array [ + "access:actions-read", + ], + } + `); + + const actionsClient = { + getAll: jest.fn().mockResolvedValueOnce([]), + }; + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts new file mode 100644 index 0000000000000..c70a13bc01c9f --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'kibana/server'; +import { ILicenseState, verifyApiAccess } from '../lib'; +import { BASE_ACTION_API_PATH } from '../../common'; + +export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState) => { + router.get( + { + path: `${BASE_ACTION_API_PATH}/_getAll`, + validate: {}, + options: { + tags: ['access:actions-read'], + }, + }, + router.handleLegacyErrors(async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + verifyApiAccess(licenseState); + if (!context.actions) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + const actionsClient = context.actions.getActionsClient(); + const result = await actionsClient.getAll(); + return res.ok({ + body: result, + }); + }) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index 33191132bece5..94f9ec1c94364 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -6,7 +6,7 @@ export { createActionRoute } from './create'; export { deleteActionRoute } from './delete'; -export { findActionRoute } from './find'; +export { getAllActionRoute } from './get_all'; export { getActionRoute } from './get'; export { updateActionRoute } from './update'; export { listActionTypesRoute } from './list_action_types'; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 999e739e77060..92e38d77314f8 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -55,6 +55,11 @@ export interface ActionResult { actionTypeId: string; name: string; config: Record; + isPreconfigured: boolean; +} + +export interface PreConfiguredAction extends ActionResult { + secrets: Record; } export interface FindActionResult extends ActionResult { diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 9d4ea69a63609..2574e73dd4f9a 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -17,6 +17,7 @@ export interface ActionGroup { export interface AlertingFrameworkHealth { isSufficientlySecure: boolean; + hasPermanentEncryptionKey: boolean; } export const BASE_ALERT_API_PATH = '/api/alert'; diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index 0e929ff457fbd..a9ff5ee8ecdc6 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -30,6 +30,7 @@ const alertsClientParams = { invalidateAPIKey: jest.fn(), logger: loggingServiceMock.create().get(), encryptedSavedObjectsPlugin: encryptedSavedObjects, + preconfiguredActions: [], }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 5538b44b69fcb..6f8478df58a53 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -13,6 +13,7 @@ import { SavedObjectReference, SavedObject, } from 'src/core/server'; +import { PreConfiguredAction } from '../../actions/server'; import { Alert, PartialAlert, @@ -53,6 +54,7 @@ interface ConstructorOptions { getUserName: () => Promise; createAPIKey: () => Promise; invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; + preconfiguredActions: PreConfiguredAction[]; } export interface FindOptions { @@ -123,6 +125,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; + private preconfiguredActions: PreConfiguredAction[]; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor({ @@ -136,6 +139,7 @@ export class AlertsClient { createAPIKey, invalidateAPIKey, encryptedSavedObjectsPlugin, + preconfiguredActions, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -147,6 +151,7 @@ export class AlertsClient { this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin; + this.preconfiguredActions = preconfiguredActions; } public async create({ data, options }: CreateOptions): Promise { @@ -659,18 +664,37 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - // Fetch action objects in bulk - const actionIds = [...new Set(alertActions.map(alertAction => alertAction.id))]; - const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' })); - const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); const actionMap = new Map(); - for (const action of bulkGetResult.saved_objects) { - if (action.error) { - throw Boom.badRequest( - `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` - ); + // map preconfigured actions + for (const alertAction of alertActions) { + const action = this.preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === alertAction.id + ); + if (action !== undefined) { + actionMap.set(action.id, action); + } + } + // Fetch action objects in bulk + // Excluding preconfigured actions to avoid an not found error, which is already mapped + const actionIds = [ + ...new Set( + alertActions + .filter(alertAction => !actionMap.has(alertAction.id)) + .map(alertAction => alertAction.id) + ), + ]; + if (actionIds.length > 0) { + const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' })); + const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); + + for (const action of bulkGetResult.saved_objects) { + if (action.error) { + throw Boom.badRequest( + `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` + ); + } + actionMap.set(action.id, action); } - actionMap.set(action.id, action); } // Extract references and set actionTypeId const references: SavedObjectReference[] = []; @@ -681,10 +705,16 @@ export class AlertsClient { name: actionRef, type: 'action', }); + const actionMapValue = actionMap.get(id); + // if action is a save object, than actionTypeId should be under attributes property + // if action is a preconfigured, than actionTypeId is the action property + const actionTypeId = actionIds.find(actionId => actionId === id) + ? actionMapValue.attributes.actionTypeId + : actionMapValue.actionTypeId; return { ...alertAction, actionRef, - actionTypeId: actionMap.get(id).attributes.actionTypeId, + actionTypeId, }; }); return { diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index 4c74ca54a0d2f..951d18a33b35f 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -28,6 +28,7 @@ const alertsClientFactoryParams: jest.Mocked = { getSpaceId: jest.fn(), spaceIdToNamespace: jest.fn(), encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.createStart(), + preconfiguredActions: [], }; const fakeRequest: Request = { headers: {}, @@ -67,6 +68,7 @@ test('creates an alerts client with proper constructor arguments', async () => { createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsPlugin: alertsClientFactoryParams.encryptedSavedObjectsPlugin, + preconfiguredActions: [], }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index fd480658e236a..734417e72733e 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PreConfiguredAction } from '../../actions/server'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server'; @@ -19,6 +20,7 @@ export interface AlertsClientFactoryOpts { getSpaceId: (request: KibanaRequest) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; + preconfiguredActions: PreConfiguredAction[]; } export class AlertsClientFactory { @@ -30,6 +32,7 @@ export class AlertsClientFactory { private getSpaceId!: (request: KibanaRequest) => string | undefined; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; private encryptedSavedObjectsPlugin!: EncryptedSavedObjectsPluginStart; + private preconfiguredActions!: PreConfiguredAction[]; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -43,6 +46,7 @@ export class AlertsClientFactory { this.securityPluginSetup = options.securityPluginSetup; this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsPlugin = options.encryptedSavedObjectsPlugin; + this.preconfiguredActions = options.preconfiguredActions; } public create( @@ -100,6 +104,7 @@ export class AlertsClientFactory { result: invalidateAPIKeyResult, }; }, + preconfiguredActions: this.preconfiguredActions, }); } } diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 90e274df3a5ee..fdca6c0a9b503 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -190,7 +190,7 @@ export class AlertingPlugin { unmuteAllAlertRoute(router, this.licenseState); muteAlertInstanceRoute(router, this.licenseState); unmuteAlertInstanceRoute(router, this.licenseState); - healthRoute(router, this.licenseState); + healthRoute(router, this.licenseState, plugins.encryptedSavedObjects); return { registerType: alertTypeRegistry.register.bind(alertTypeRegistry), @@ -218,6 +218,7 @@ export class AlertingPlugin { getSpaceId(request: KibanaRequest) { return spaces?.getSpaceId(request); }, + preconfiguredActions: plugins.actions.preconfiguredActions, }); taskRunnerFactory.initialize({ diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index 9efe020bc10c4..42c83a7c04deb 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -10,6 +10,7 @@ import { mockHandlerArguments } from './_mock_handler_arguments'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockLicenseState } from '../lib/license_state.mock'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; jest.mock('../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -24,7 +25,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [config] = router.get.mock.calls[0]; @@ -35,7 +38,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -58,11 +63,37 @@ describe('healthRoute', () => { `); }); + it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => { + const router: RouterMock = mockRouter.create(); + + const licenseState = mockLicenseState(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = true; + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const elasticsearch = elasticsearchServiceMock.createSetup(); + elasticsearch.adminClient.callAsInternalUser.mockReturnValue(Promise.resolve({})); + + const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "hasPermanentEncryptionKey": false, + "isSufficientlySecure": true, + }, + } + `); + }); + it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -73,6 +104,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": true, }, } @@ -83,7 +115,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -94,6 +128,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": true, }, } @@ -104,7 +139,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -117,6 +154,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": false, }, } @@ -127,7 +165,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -140,6 +180,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": false, }, } @@ -150,7 +191,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -163,6 +206,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": true, }, } diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index 29c2f3c5730f4..fa2358a1f181c 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -14,6 +14,7 @@ import { import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { AlertingFrameworkHealth } from '../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; interface XPackUsageSecurity { security?: { @@ -26,7 +27,11 @@ interface XPackUsageSecurity { }; } -export function healthRoute(router: IRouter, licenseState: LicenseState) { +export function healthRoute( + router: IRouter, + licenseState: LicenseState, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { router.get( { path: '/api/alert/_health', @@ -54,6 +59,7 @@ export function healthRoute(router: IRouter, licenseState: LicenseState) { const frameworkHealth: AlertingFrameworkHealth = { isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), + hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey, }; return res.ok({ diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 5de82a9ee8788..54dd4704edfc0 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -16,6 +16,8 @@ exports[`Error ERROR_EXC_HANDLED 1`] = `undefined`; exports[`Error ERROR_EXC_MESSAGE 1`] = `undefined`; +exports[`Error ERROR_EXC_TYPE 1`] = `undefined`; + exports[`Error ERROR_GROUP_ID 1`] = `"grouping key"`; exports[`Error ERROR_LOG_LEVEL 1`] = `undefined`; @@ -144,6 +146,8 @@ exports[`Span ERROR_EXC_HANDLED 1`] = `undefined`; exports[`Span ERROR_EXC_MESSAGE 1`] = `undefined`; +exports[`Span ERROR_EXC_TYPE 1`] = `undefined`; + exports[`Span ERROR_GROUP_ID 1`] = `undefined`; exports[`Span ERROR_LOG_LEVEL 1`] = `undefined`; @@ -272,6 +276,8 @@ exports[`Transaction ERROR_EXC_HANDLED 1`] = `undefined`; exports[`Transaction ERROR_EXC_MESSAGE 1`] = `undefined`; +exports[`Transaction ERROR_EXC_TYPE 1`] = `undefined`; + exports[`Transaction ERROR_GROUP_ID 1`] = `undefined`; exports[`Transaction ERROR_LOG_LEVEL 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index bc435179762a2..49840d2157af7 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -153,8 +153,9 @@ Array [ }, Object { "key": "stress_monitor_gc_stress_threshold", - "type": "boolean", - "validationName": "(\\"true\\" | \\"false\\")", + "type": "float", + "validationError": "Must be a number between 0.000 and 1", + "validationName": "numberFloatRt", }, Object { "key": "stress_monitor_system_cpu_relief_threshold", diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts index bb050076b9f9a..2e10c74378549 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts @@ -20,7 +20,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.enableLogCorrelation.description', { defaultMessage: - "A boolean specifying if the agent should integrate into SLF4J's MDC to enable trace-log correlation. If set to `true`, the agent will set the `trace.id` and `transaction.id` for the currently active spans and transactions to the MDC. While it's allowed to enable this setting at runtime, you can't disable it without a restart." + "A boolean specifying if the agent should integrate into SLF4J's MDC to enable trace-log correlation. If set to `true`, the agent will set the `trace.id` and `transaction.id` for the currently active spans and transactions to the MDC. Since Java agent version 1.16.0, the agent also adds `error.id` of captured error to the MDC just before the error message is logged. NOTE: While it's allowed to enable this setting at runtime, you can't disable it without a restart." } ), includeAgents: ['java'] @@ -41,7 +41,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.circuitBreakerEnabled.description', { defaultMessage: - 'A boolean specifying whether the circuit breaker should be enabled or not. When enabled, the agent periodically polls stress monitors to detect system/process/JVM stress state. If ANY of the monitors detects a stress indication, the agent will become inactive, as if the `active` configuration option has been set to `false`, thus reducing resource consumption to a minimum. When inactive, the agent continues polling the same monitors in order to detect whether the stress state has been relieved. If ALL monitors approve that the system/process/JVM is not under stress anymore, the agent will resume and become fully functional.' + 'A boolean specifying whether the circuit breaker should be enabled or not. When enabled, the agent periodically polls stress monitors to detect system/process/JVM stress state. If ANY of the monitors detects a stress indication, the agent will pause, as if the `recording` configuration option has been set to `false`, thus reducing resource consumption to a minimum. When paused, the agent continues polling the same monitors in order to detect whether the stress state has been relieved. If ALL monitors approve that the system/process/JVM is not under stress anymore, the agent will resume and become fully functional.' } ), includeAgents: ['java'] @@ -52,7 +52,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.stressMonitorGcStressThreshold.label', { defaultMessage: 'Stress monitor gc stress threshold' } ), - type: 'boolean', + type: 'float', category: 'Circuit-Breaker', defaultValue: '0.95', description: i18n.translate( @@ -155,7 +155,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.profilingInferredSpansEnabled.description', { defaultMessage: - 'Set to `true` to make the agent create spans for method executions based on async-profiler, a sampling aka statistical profiler. Due to the nature of how sampling profilers work, the duration of the inferred spans are not exact, but only estimations. The `profiling_inferred_spans_sampling_interval` lets you fine tune the trade-off between accuracy and overhead. The inferred spans are created after a profiling session has ended. This means there is a delay between the regular and the inferred spans being visible in the UI. This feature is not available on Windows' + 'Set to `true` to make the agent create spans for method executions based on async-profiler, a sampling aka statistical profiler. Due to the nature of how sampling profilers work, the duration of the inferred spans are not exact, but only estimations. The `profiling_inferred_spans_sampling_interval` lets you fine tune the trade-off between accuracy and overhead. The inferred spans are created after a profiling session has ended. This means there is a delay between the regular and the inferred spans being visible in the UI. NOTE: This feature is not available on Windows.' } ), includeAgents: ['java'] @@ -209,7 +209,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.profilingInferredSpansIncludedClasses.description', { defaultMessage: - 'If set, the agent will only create inferred spans for methods which match this list. Setting a value may slightly increase performance and can reduce clutter by only creating spans for the classes you are interested in. Example: `org.example.myapp.*` This option supports the wildcard `*`, which matches zero or more characters. Examples: `/foo/*/bar/*/baz*`, `*foo*`. Matching is case insensitive by default. Prepending an element with `(?-i)` makes the matching case sensitive.' + 'If set, the agent will only create inferred spans for methods which match this list. Setting a value may slightly reduce overhead and can reduce clutter by only creating spans for the classes you are interested in. This option supports the wildcard `*`, which matches zero or more characters. Example: `org.example.myapp.*`. Matching is case insensitive by default. Prepending an element with `(?-i)` makes the matching case sensitive.' } ), includeAgents: ['java'] @@ -228,7 +228,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.profilingInferredSpansExcludedClasses.description', { defaultMessage: - 'Excludes classes for which no profiler-inferred spans should be created. This option supports the wildcard `*`, which matches zero or more characters. Examples: `/foo/*/bar/*/baz*`, `*foo*`. Matching is case insensitive by default. Prepending an element with `(?-i)` makes the matching case sensitive.' + 'Excludes classes for which no profiler-inferred spans should be created. This option supports the wildcard `*`, which matches zero or more characters. Matching is case insensitive by default. Prepending an element with `(?-i)` makes the matching case sensitive.' } ), includeAgents: ['java'] diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index bc1b346f50da7..d5c3f91eb9247 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -60,6 +60,7 @@ export const ERROR_LOG_LEVEL = 'error.log.level'; export const ERROR_LOG_MESSAGE = 'error.log.message'; export const ERROR_EXC_MESSAGE = 'error.exception.message'; // only to be used in es queries, since error.exception is now an array export const ERROR_EXC_HANDLED = 'error.exception.handled'; // only to be used in es queries, since error.exception is now an array +export const ERROR_EXC_TYPE = 'error.exception.type'; export const ERROR_PAGE_URL = 'error.page.url'; // METRICS diff --git a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index b9ac9d5431700..982ad558dc91d 100644 --- a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -73,6 +73,7 @@ Object { "error.log.message", "error.exception.message", "error.exception.handled", + "error.exception.type", "error.culprit", "error.grouping_key", "@timestamp", @@ -148,6 +149,7 @@ Object { "error.log.message", "error.exception.message", "error.exception.handled", + "error.exception.type", "error.culprit", "error.grouping_key", "@timestamp", diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 8ea6df5a9898a..5221d737866f4 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -8,6 +8,7 @@ import { ERROR_CULPRIT, ERROR_EXC_HANDLED, ERROR_EXC_MESSAGE, + ERROR_EXC_TYPE, ERROR_GROUP_ID, ERROR_LOG_MESSAGE } from '../../../common/elasticsearch_fieldnames'; @@ -67,6 +68,7 @@ export async function getErrorGroups({ ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, ERROR_CULPRIT, ERROR_GROUP_ID, '@timestamp' @@ -99,6 +101,7 @@ export async function getErrorGroups({ exception?: Array<{ handled?: boolean; message?: string; + type?: string; }>; culprit: APMError['error']['culprit']; grouping_key: APMError['error']['grouping_key']; @@ -120,7 +123,8 @@ export async function getErrorGroups({ culprit: source.error.culprit, groupId: source.error.grouping_key, latestOccurrenceAt: source['@timestamp'], - handled: source.error.exception?.[0].handled + handled: source.error.exception?.[0].handled, + type: source.error.exception?.[0].type }; }); diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index 9b210c2aa05ad..d92af587d0e92 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -61,13 +61,6 @@ export type CasesConnectorConfiguration = rt.TypeOf action.actionTypeId === CASE_SERVICE_NOW_ACTION + ); + return response.ok({ body: results }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 2b8247066bfc3..62e21392f7110 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -11,6 +11,7 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; interface CloudConfigType { id?: string; + resetPasswordUrl?: string; } interface CloudSetupDependencies { @@ -26,13 +27,13 @@ export class CloudPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { - const { id } = this.initializerContext.config.get(); + const { id, resetPasswordUrl } = this.initializerContext.config.get(); const isCloudEnabled = getIsCloudEnabled(id); if (home) { home.environment.update({ cloud: isCloudEnabled }); if (isCloudEnabled) { - home.tutorials.setVariable('cloud', { id }); + home.tutorials.setVariable('cloud', { id, resetPasswordUrl }); } } diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index 77e493dc3b7dc..d899b45aebdfe 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -21,6 +21,7 @@ const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), id: schema.maybe(schema.string()), apm: schema.maybe(apmConfigSchema), + resetPasswordUrl: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -28,6 +29,7 @@ export type CloudConfigType = TypeOf; export const config: PluginConfigDescriptor = { exposeToBrowser: { id: true, + resetPasswordUrl: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/endpoint/kibana.json b/x-pack/plugins/endpoint/kibana.json index 5b8bec7777406..4b48c83fb0e7c 100644 --- a/x-pack/plugins/endpoint/kibana.json +++ b/x-pack/plugins/endpoint/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "endpoint"], - "requiredPlugins": ["features", "embeddable", "data", "dataEnhanced"], + "requiredPlugins": ["features", "embeddable", "data", "dataEnhanced", "ingestManager"], "server": true, "ui": true } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx index b110d32442c2c..858dac864b58a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx @@ -12,7 +12,7 @@ import { useNavigateToAppEventHandler } from '../hooks/use_navigate_to_app_event export type LinkToAppProps = EuiLinkProps & { /** the app id - normally the value of the `id` in that plugin's `kibana.json` */ appId: string; - /** Any app specic path (route) */ + /** Any app specific path (route) */ appPath?: string; appState?: any; onClick?: MouseEventHandler; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 89a6302351a54..82ac95160519c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -18,6 +18,7 @@ import { PolicyList } from './view/policy'; import { PolicyDetails } from './view/policy'; import { HeaderNavigation } from './components/header_nav'; import { AppRootProvider } from './view/app_root_provider'; +import { Setup } from './view/setup'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -48,6 +49,7 @@ const AppRoot: React.FunctionComponent = React.memo( ({ history, store, coreStart, depsStart }) => { return ( + RenderResul /** * Mocked app root context renderer */ -interface AppContextTestRender { +export interface AppContextTestRender { store: ReturnType; history: ReturnType; coreStart: ReturnType; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/dependencies_start_mock.ts b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/dependencies_start_mock.ts index 00cf0bca57e66..d3fc653f4c9ba 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/dependencies_start_mock.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/dependencies_start_mock.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IngestManagerStart } from '../../../../../ingest_manager/public'; import { dataPluginMock, Start as DataPublicStartMock, @@ -32,6 +33,7 @@ type DataMock = Omit & { */ export interface DepsStartMock { data: DataMock; + ingestManager: IngestManagerStart; } /** @@ -54,5 +56,6 @@ export const depsStartMock: () => DepsStartMock = () => { return { data: dataMock, + ingestManager: { success: true }, }; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts index d4c2602e34387..20aa973ffc93d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList, HostStatus } from '../../../../../common/types'; +import { HostInfo, HostResultList, HostStatus } from '../../../../../common/types'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; export const mockHostResultList: (options?: { @@ -40,3 +40,14 @@ export const mockHostResultList: (options?: { }; return mock; }; + +/** + * returns a mocked API response for retrieving a single host metadata + */ +export const mockHostDetailsApiResult = (): HostInfo => { + const generator = new EndpointDocGenerator('seed'); + return { + metadata: generator.generateHostMetadata(), + host_status: HostStatus.ERROR, + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx index 37080e8568350..90829f7ad4cbe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx @@ -28,6 +28,7 @@ import { useHostListSelector } from './hooks'; import { urlFromQueryParams } from './url_from_query_params'; import { FormattedDateAndTime } from '../formatted_date_time'; import { uiQueryParams, detailsData, detailsError } from './../../store/hosts/selectors'; +import { LinkToApp } from '../../components/link_to_app'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -37,6 +38,7 @@ const HostIds = styled(EuiListGroupItem)` `; const HostDetails = memo(({ details }: { details: HostMetadata }) => { + const { appId, appPath, url } = useHostLogsUrl(details.host.id); const detailsResultsUpper = useMemo(() => { return [ { @@ -113,6 +115,20 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { listItems={detailsResultsLower} data-test-subj="hostDetailsLowerList" /> + +

+ + + +

); }); @@ -170,3 +186,15 @@ export const HostDetailsFlyout = () => { ); }; + +const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { + const { services } = useKibana(); + return useMemo(() => { + const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; + return { + url: `${services.application.getUrlForApp('logs')}${appPath}`, + appId: 'logs', + appPath, + }; + }, [hostId, services.application]); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index f6dfae99c1b11..c3ff41268e3db 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -6,40 +6,26 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { I18nProvider } from '@kbn/i18n/react'; -import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; -import { appStoreFactory } from '../../store'; -import { RouteCapture } from '../route_capture'; -import { createMemoryHistory, MemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { fireEvent } from '@testing-library/react'; import { AppAction } from '../../types'; import { HostList } from './index'; -import { mockHostResultList } from '../../store/hosts/mock_host_result_list'; +import { + mockHostDetailsApiResult, + mockHostResultList, +} from '../../store/hosts/mock_host_result_list'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; +import { HostInfo } from '../../../../../common/types'; describe('when on the hosts page', () => { - let render: () => reactTestingLibrary.RenderResult; - let history: MemoryHistory; - let store: ReturnType; + let render: () => ReturnType; + let history: AppContextTestRender['history']; + let store: AppContextTestRender['store']; + let coreStart: AppContextTestRender['coreStart']; beforeEach(async () => { - history = createMemoryHistory(); - store = appStoreFactory(); - render = () => { - return reactTestingLibrary.render( - - - - - - - - - - - - ); - }; + const mockedContext = createAppRootMockRenderer(); + ({ history, store, coreStart } = mockedContext); + render = () => mockedContext.render(); }); it('should show a table', async () => { @@ -56,7 +42,7 @@ describe('when on the hosts page', () => { expect(e).not.toBeNull(); }); }); - describe('when data loads', () => { + describe('when list data loads', () => { beforeEach(() => { reactTestingLibrary.act(() => { const action: AppAction = { @@ -76,6 +62,16 @@ describe('when on the hosts page', () => { describe('when the user clicks the hostname in the table', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { + const hostDetailsApiResponse = mockHostDetailsApiResult(); + + coreStart.http.get.mockReturnValue(Promise.resolve(hostDetailsApiResponse)); + reactTestingLibrary.act(() => { + store.dispatch({ + type: 'serverReturnedHostDetails', + payload: hostDetailsApiResponse, + }); + }); + renderResult = render(); const detailsLink = await renderResult.findByTestId('hostnameCellLink'); if (detailsLink) { @@ -93,19 +89,71 @@ describe('when on the hosts page', () => { }); describe('when there is a selected host in the url', () => { + let hostDetails: HostInfo; beforeEach(() => { + const { + host_status, + metadata: { host, ...details }, + } = mockHostDetailsApiResult(); + hostDetails = { + host_status, + metadata: { + ...details, + host: { + ...host, + id: '1', + }, + }, + }; + + coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); + coreStart.application.getUrlForApp.mockReturnValue('/app/logs'); + reactTestingLibrary.act(() => { history.push({ ...history.location, search: '?selected_host=1', }); }); + reactTestingLibrary.act(() => { + store.dispatch({ + type: 'serverReturnedHostDetails', + payload: hostDetails, + }); + }); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should show the flyout', () => { const renderResult = render(); return renderResult.findByTestId('hostDetailsFlyout').then(flyout => { expect(flyout).not.toBeNull(); }); }); + it('should include the link to logs', async () => { + const renderResult = render(); + const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); + expect(linkToLogs).not.toBeNull(); + expect(linkToLogs.textContent).toEqual('Endpoint Logs'); + expect(linkToLogs.getAttribute('href')).toEqual( + "/app/logs/stream?logFilter=(expression:'host.id:1',kind:kuery)" + ); + }); + describe('when link to logs is clicked', () => { + beforeEach(async () => { + const renderResult = render(); + const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); + reactTestingLibrary.act(() => { + fireEvent.click(linkToLogs); + }); + }); + + it('should navigate to logs without full page refresh', async () => { + // FIXME: this is not working :( + expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); + }); + }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/setup.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/setup.tsx new file mode 100644 index 0000000000000..a826e1f30f75d --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/setup.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as React from 'react'; +import { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'kibana/public'; +import { IngestManagerStart } from '../../../../../ingest_manager/public'; + +export const Setup: React.FunctionComponent<{ + ingestManager: IngestManagerStart; + notifications: NotificationsStart; +}> = ({ ingestManager, notifications }) => { + React.useEffect(() => { + const defaultText = i18n.translate('xpack.endpoint.ingestToastMessage', { + defaultMessage: 'Ingest Manager failed during its setup.', + }); + + const title = i18n.translate('xpack.endpoint.ingestToastTitle', { + defaultMessage: 'App failed to initialize', + }); + + const displayToastWithModal = (text: string) => { + const errorText = new Error(defaultText); + // we're leveraging the notification's error toast which is usually used for displaying stack traces of an + // actually Error. Instead of displaying a stack trace we'll display the more detailed error text when the + // user clicks `See the full error` button to see the modal + errorText.stack = text; + notifications.toasts.addError(errorText, { + title, + }); + }; + + const displayToast = () => { + notifications.toasts.addDanger({ + title, + text: defaultText, + }); + }; + + if (!ingestManager.success) { + if (ingestManager.error) { + displayToastWithModal(ingestManager.error.message); + } else { + displayToast(); + } + } + }, [ingestManager, notifications.toasts]); + + return null; +}; diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index ee5bbe71ae8aa..9964454add801 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -8,6 +8,7 @@ import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public' import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { i18n } from '@kbn/i18n'; +import { IngestManagerStart } from '../../ingest_manager/public'; import { ResolverEmbeddableFactory } from './embeddables/resolver'; export type EndpointPluginStart = void; @@ -18,6 +19,7 @@ export interface EndpointPluginSetupDependencies { } export interface EndpointPluginStartDependencies { data: DataPublicPluginStart; + ingestManager: IngestManagerStart; } /** diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/feature.ts index 82fcc33f5c8ce..ef32a8a80a0bd 100644 --- a/x-pack/plugins/features/common/feature.ts +++ b/x-pack/plugins/features/common/feature.ts @@ -7,6 +7,7 @@ import { RecursiveReadonly } from '@kbn/utility-types'; import { FeatureKibanaPrivileges } from './feature_kibana_privileges'; import { SubFeatureConfig, SubFeature } from './sub_feature'; +import { ReservedKibanaPrivilege } from './reserved_kibana_privilege'; /** * Interface for registering a feature. @@ -122,8 +123,8 @@ export interface FeatureConfig { * @private */ reserved?: { - privilege: FeatureKibanaPrivileges; description: string; + privileges: ReservedKibanaPrivilege[]; }; } diff --git a/x-pack/plugins/features/common/reserved_kibana_privilege.ts b/x-pack/plugins/features/common/reserved_kibana_privilege.ts new file mode 100644 index 0000000000000..0186011382e84 --- /dev/null +++ b/x-pack/plugins/features/common/reserved_kibana_privilege.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FeatureKibanaPrivileges } from '.'; + +export interface ReservedKibanaPrivilege { + id: string; + privilege: FeatureKibanaPrivileges; +} diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index 5b4f7728c9f31..2039f8f6acda2 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -110,19 +110,24 @@ describe('FeatureRegistry', () => { ], privilegesTooltip: 'some fancy tooltip', reserved: { - privilege: { - catalogue: ['foo'], - management: { - foo: ['bar'], - }, - app: ['app1'], - savedObject: { - all: ['space', 'etc', 'telemetry'], - read: ['canvas', 'config', 'url'], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['space', 'etc', 'telemetry'], + read: ['canvas', 'config', 'url'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, }, - api: ['someApiEndpointTag', 'anotherEndpointTag'], - ui: ['allowsFoo', 'showBar', 'showBaz'], - }, + ], description: 'some completely adequate description', }, }; @@ -264,13 +269,18 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'foo', - privilege: { - ui: [], - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + ui: [], + savedObject: { + all: [], + read: [], + }, + }, }, - }, + ], }, }; @@ -278,7 +288,7 @@ describe('FeatureRegistry', () => { featureRegistry.register(feature); const result = featureRegistry.getAll(); - const reservedPrivilege = result[0]!.reserved!.privilege; + const reservedPrivilege = result[0]!.reserved!.privileges[0].privilege; expect(reservedPrivilege.savedObject.all).toEqual(['telemetry']); expect(reservedPrivilege.savedObject.read).toEqual(['config', 'url']); }); @@ -520,14 +530,19 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, }, - ui: [], - app: ['foo', 'bar', 'baz'], - }, + ], }, }; @@ -546,14 +561,19 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar'], + }, }, - ui: [], - app: ['foo', 'bar'], - }, + ], }, }; @@ -666,15 +686,20 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - catalogue: ['foo', 'bar', 'baz'], - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, - ui: [], - app: [], - }, + ], }, }; @@ -694,15 +719,20 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - catalogue: ['foo', 'bar'], - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo', 'bar'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, - ui: [], - app: [], - }, + ], }, }; @@ -840,18 +870,23 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - catalogue: ['bar'], - management: { - kibana: ['hey-there'], - }, - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, - ui: [], - app: [], - }, + ], }, }; @@ -874,18 +909,23 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - catalogue: ['bar'], - management: { - kibana: ['hey-there'], - }, - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, - ui: [], - app: [], - }, + ], }, }; @@ -896,6 +936,78 @@ describe('FeatureRegistry', () => { ); }); + it('allows multiple reserved feature privileges to be registered', () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'my reserved privileges', + privileges: [ + { + id: 'a_reserved_1', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + { + id: 'a_reserved_2', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature); + const result = featureRegistry.getAll(); + expect(result).toHaveLength(1); + expect(result[0].reserved?.privileges).toHaveLength(2); + }); + + it('does not allow reserved privilege ids to start with "reserved_"', () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'my reserved privileges', + privileges: [ + { + id: 'reserved_1', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"child \\"reserved\\" fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"id\\" fails because [\\"id\\" with value \\"reserved_1\\" fails to match the required pattern: /^(?!reserved_)[a-zA-Z0-9_-]+$/]]]]"` + ); + }); + it('cannot register feature after getAll has been called', () => { const feature1: FeatureConfig = { id: 'test-feature', diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index 73a353cd27471..6140b7ac87ce0 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -39,9 +39,9 @@ export class FeatureRegistry { function applyAutomaticPrivilegeGrants(feature: FeatureConfig): FeatureConfig { const allPrivilege = feature.privileges?.all; const readPrivilege = feature.privileges?.read; - const reservedPrivilege = feature.reserved?.privilege; + const reservedPrivileges = (feature.reserved?.privileges ?? []).map(rp => rp.privilege); - applyAutomaticAllPrivilegeGrants(allPrivilege, reservedPrivilege); + applyAutomaticAllPrivilegeGrants(allPrivilege, ...reservedPrivileges); applyAutomaticReadPrivilegeGrants(readPrivilege); return feature; diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index fdeceb30b4e3d..403d9586bf160 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -18,6 +18,7 @@ const prohibitedFeatureIds: Array = ['catalogue', 'managem const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; const subFeaturePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/; +const reservedFeaturePrrivilegePartRegex = /^(?!reserved_)[a-zA-Z0-9_-]+$/; export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/; const managementSchema = Joi.object().pattern( @@ -118,8 +119,17 @@ const schema = Joi.object({ }), privilegesTooltip: Joi.string(), reserved: Joi.object({ - privilege: privilegeSchema.required(), description: Joi.string().required(), + privileges: Joi.array() + .items( + Joi.object({ + id: Joi.string() + .regex(reservedFeaturePrrivilegePartRegex) + .required(), + privilege: privilegeSchema.required(), + }) + ) + .required(), }), }); @@ -209,7 +219,9 @@ export function validateFeature(feature: FeatureConfig) { privilegeEntries.push(...Object.entries(feature.privileges)); } if (feature.reserved) { - privilegeEntries.push(['reserved', feature.reserved.privilege]); + feature.reserved.privileges.forEach(reservedPrivilege => { + privilegeEntries.push([reservedPrivilege.id, reservedPrivilege.privilege]); + }); } if (privilegeEntries.length === 0) { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx index bc1917b2da966..cec97fb925eef 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx @@ -23,7 +23,7 @@ export const MaxShingleSizeParameter = ({ defaultToggleValue }: Props) => ( })} description={i18n.translate('xpack.idxMgmt.mappingsEditor.maxShingleSizeFieldDescription', { defaultMessage: - 'The default is three shingle subfields. More subfields enables more specific queries, but increases index size.', + 'The default is three shingle subfields. More subfields enable more specific queries, but increase index size.', })} defaultToggleValue={defaultToggleValue} > diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx index da4b8e6f6eef2..95630a6981843 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx @@ -51,7 +51,8 @@ const openModalWithJsonContent = ({ find, waitFor }: TestBed) => async (json: an }); }; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/59030 +describe.skip('', () => { test('it should forward valid mapping definition', async () => { const mappingsToLoad = { properties: { diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 0903787dd05c4..2e43ede2480ce 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -320,11 +320,11 @@ interface ExpressionRowProps { const StyledExpressionRow = euiStyled(EuiFlexGroup)` display: flex; flex-wrap: wrap; - margin: 0 -${props => props.theme.eui.euiSizeXS}; + margin: 0 -4px; `; const StyledExpression = euiStyled.div` - padding: 0 ${props => props.theme.eui.euiSizeXS}; + padding: 0 4px; `; export const ExpressionRow: React.FC = props => { diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx index 55365c3d4c2ba..d84e46d08a287 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx @@ -23,6 +23,7 @@ export function validateMetricThreshold({ timeWindowSize: string[]; threshold0: string[]; threshold1: string[]; + metric: string[]; }; } = {}; validationResult.errors = errors; @@ -41,6 +42,7 @@ export function validateMetricThreshold({ timeWindowSize: [], threshold0: [], threshold1: [], + metric: [], }; if (!c.aggType) { errors[id].aggField.push( @@ -73,6 +75,14 @@ export function validateMetricThreshold({ }) ); } + + if (!c.metric && c.aggType !== 'count') { + errors[id].metric.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { + defaultMessage: 'Metric is required.', + }) + ); + } }); return validationResult; diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index ab039be8e7c22..b897c03e89f82 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -41,7 +41,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }, { id: 'test-logs-bar', - enabled: false, + enabled: true, dataset: 'bar', config: { barVar: { value: 'bar-value' }, @@ -119,7 +119,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }, { id: 'test-logs-bar', - enabled: false, + enabled: true, dataset: 'bar', barVar: 'bar-value', barVar2: [1, 2], @@ -140,6 +140,44 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); + it('returns agent datasource config without disabled streams', () => { + expect( + storedDatasourceToAgentDatasource({ + ...mockDatasource, + inputs: [ + { + ...mockInput, + streams: [{ ...mockInput.streams[0] }, { ...mockInput.streams[1], enabled: false }], + }, + ], + }) + ).toEqual({ + id: 'mock-datasource', + namespace: 'default', + enabled: true, + use_output: 'default', + inputs: [ + { + type: 'test-logs', + enabled: true, + inputVar: 'input-value', + inputVar3: { + testField: 'test', + }, + streams: [ + { + id: 'test-logs-foo', + enabled: true, + dataset: 'foo', + fooVar: 'foo-value', + fooVar2: [1, 2], + }, + ], + }, + ], + }); + }); + it('returns agent datasource config without disabled inputs', () => { expect( storedDatasourceToAgentDatasource({ diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index f58eaacb7be67..20bbbec8919d6 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -51,14 +51,16 @@ export const storedDatasourceToAgentDatasource = ( const fullInput = { ...input, ...Object.entries(input.config || {}).reduce(configReducer, {}), - streams: input.streams.map(stream => { - const fullStream = { - ...stream, - ...Object.entries(stream.config || {}).reduce(configReducer, {}), - }; - delete fullStream.config; - return fullStream; - }), + streams: input.streams + .filter(stream => stream.enabled) + .map(stream => { + const fullStream = { + ...stream, + ...Object.entries(stream.config || {}).reduce(configReducer, {}), + }; + delete fullStream.config; + return fullStream; + }), }; delete fullInput.config; return fullInput; diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index efa6621001038..5524e7505d74b 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -252,6 +252,7 @@ export enum IngestAssetType { export enum DefaultPackages { base = 'base', system = 'system', + endpoint = 'endpoint', } export interface IndexTemplate { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx index 356739af1ff9a..0e8763cb2d4c0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx @@ -6,26 +6,38 @@ import React, { useState, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiText, + EuiTextColor, EuiSpacer, EuiButtonEmpty, EuiTitle, + EuiIconTip, } from '@elastic/eui'; import { DatasourceInput, RegistryVarsEntry } from '../../../../types'; -import { isAdvancedVar } from '../services'; +import { isAdvancedVar, DatasourceConfigValidationResults, validationHasErrors } from '../services'; import { DatasourceInputVarField } from './datasource_input_var_field'; export const DatasourceInputConfig: React.FunctionComponent<{ packageInputVars?: RegistryVarsEntry[]; datasourceInput: DatasourceInput; updateDatasourceInput: (updatedInput: Partial) => void; -}> = ({ packageInputVars, datasourceInput, updateDatasourceInput }) => { + inputVarsValidationResults: DatasourceConfigValidationResults; + forceShowErrors?: boolean; +}> = ({ + packageInputVars, + datasourceInput, + updateDatasourceInput, + inputVarsValidationResults, + forceShowErrors, +}) => { // Showing advanced options toggle state const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); + // Errors state + const hasErrors = forceShowErrors && validationHasErrors(inputVarsValidationResults); + const requiredVars: RegistryVarsEntry[] = []; const advancedVars: RegistryVarsEntry[] = []; @@ -40,15 +52,36 @@ export const DatasourceInputConfig: React.FunctionComponent<{ } return ( - - + + -

- -

+ + +

+ + + +

+
+ {hasErrors ? ( + + + } + position="right" + type="alert" + iconProps={{ color: 'danger' }} + /> + + ) : null} +
@@ -60,7 +93,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{

- + {requiredVars.map(varDef => { const { name: varName, type: varType } = varDef; @@ -81,6 +114,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ }, }); }} + errors={inputVarsValidationResults.config![varName]} + forceShowErrors={forceShowErrors} /> ); @@ -123,6 +158,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ }, }); }} + errors={inputVarsValidationResults.config![varName]} + forceShowErrors={forceShowErrors} /> ); @@ -132,6 +169,6 @@ export const DatasourceInputConfig: React.FunctionComponent<{ ) : null}
-
+ ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx index 74b08f48df12d..6b0c68ccb7d3f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx @@ -17,8 +17,10 @@ import { EuiButtonIcon, EuiHorizontalRule, EuiSpacer, + EuiIconTip, } from '@elastic/eui'; import { DatasourceInput, DatasourceInputStream, RegistryInput } from '../../../../types'; +import { DatasourceInputValidationResults, validationHasErrors } from '../services'; import { DatasourceInputConfig } from './datasource_input_config'; import { DatasourceInputStreamConfig } from './datasource_input_stream_config'; @@ -32,10 +34,21 @@ export const DatasourceInputPanel: React.FunctionComponent<{ packageInput: RegistryInput; datasourceInput: DatasourceInput; updateDatasourceInput: (updatedInput: Partial) => void; -}> = ({ packageInput, datasourceInput, updateDatasourceInput }) => { + inputValidationResults: DatasourceInputValidationResults; + forceShowErrors?: boolean; +}> = ({ + packageInput, + datasourceInput, + updateDatasourceInput, + inputValidationResults, + forceShowErrors, +}) => { // Showing streams toggle state const [isShowingStreams, setIsShowingStreams] = useState(false); + // Errors state + const hasErrors = forceShowErrors && validationHasErrors(inputValidationResults); + return ( {/* Header / input-level toggle */} @@ -43,9 +56,32 @@ export const DatasourceInputPanel: React.FunctionComponent<{ -

{packageInput.title || packageInput.type}

- + + + +

+ + {packageInput.title || packageInput.type} + +

+
+
+ {hasErrors ? ( + + + } + position="right" + type="alert" + iconProps={{ color: 'danger' }} + /> + + ) : null} +
} checked={datasourceInput.enabled} onChange={e => { @@ -122,6 +158,8 @@ export const DatasourceInputPanel: React.FunctionComponent<{ packageInputVars={packageInput.vars} datasourceInput={datasourceInput} updateDatasourceInput={updateDatasourceInput} + inputVarsValidationResults={{ config: inputValidationResults.config }} + forceShowErrors={forceShowErrors} /> @@ -165,6 +203,10 @@ export const DatasourceInputPanel: React.FunctionComponent<{ updateDatasourceInput(updatedInput); }} + inputStreamValidationResults={ + inputValidationResults.streams![datasourceInputStream.id] + } + forceShowErrors={forceShowErrors} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx index 3bf5b2bb4c0f0..43e8f5a2c060d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx @@ -7,26 +7,38 @@ import React, { useState, Fragment } from 'react'; import ReactMarkdown from 'react-markdown'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiText, EuiSpacer, EuiButtonEmpty, + EuiTextColor, + EuiIconTip, } from '@elastic/eui'; import { DatasourceInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types'; -import { isAdvancedVar } from '../services'; +import { isAdvancedVar, DatasourceConfigValidationResults, validationHasErrors } from '../services'; import { DatasourceInputVarField } from './datasource_input_var_field'; export const DatasourceInputStreamConfig: React.FunctionComponent<{ packageInputStream: RegistryStream; datasourceInputStream: DatasourceInputStream; updateDatasourceInputStream: (updatedStream: Partial) => void; -}> = ({ packageInputStream, datasourceInputStream, updateDatasourceInputStream }) => { + inputStreamValidationResults: DatasourceConfigValidationResults; + forceShowErrors?: boolean; +}> = ({ + packageInputStream, + datasourceInputStream, + updateDatasourceInputStream, + inputStreamValidationResults, + forceShowErrors, +}) => { // Showing advanced options toggle state const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); + // Errors state + const hasErrors = forceShowErrors && validationHasErrors(inputStreamValidationResults); + const requiredVars: RegistryVarsEntry[] = []; const advancedVars: RegistryVarsEntry[] = []; @@ -41,10 +53,33 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ } return ( - - + + + + + {packageInputStream.title || packageInputStream.dataset} + + + {hasErrors ? ( + + + } + position="right" + type="alert" + iconProps={{ color: 'danger' }} + /> + + ) : null} + + } checked={datasourceInputStream.enabled} onChange={e => { const enabled = e.target.checked; @@ -62,7 +97,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ ) : null} - + {requiredVars.map(varDef => { const { name: varName, type: varType } = varDef; @@ -83,6 +118,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ }, }); }} + errors={inputStreamValidationResults.config![varName]} + forceShowErrors={forceShowErrors} /> ); @@ -125,6 +162,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ }, }); }} + errors={inputStreamValidationResults.config![varName]} + forceShowErrors={forceShowErrors} /> ); @@ -134,6 +173,6 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ ) : null}
- + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx index bcb99eed88ac0..846a807f9240d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import ReactMarkdown from 'react-markdown'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiFieldText, EuiComboBox, EuiText, EuiCodeEditor } from '@elastic/eui'; @@ -16,12 +16,20 @@ export const DatasourceInputVarField: React.FunctionComponent<{ varDef: RegistryVarsEntry; value: any; onChange: (newValue: any) => void; -}> = ({ varDef, value, onChange }) => { + errors?: string[] | null; + forceShowErrors?: boolean; +}> = ({ varDef, value, onChange, errors: varErrors, forceShowErrors }) => { + const [isDirty, setIsDirty] = useState(false); + const { multi, required, type, title, name, description } = varDef; + const isInvalid = (isDirty || forceShowErrors) && !!varErrors; + const errors = isInvalid ? varErrors : null; + const renderField = () => { - if (varDef.multi) { + if (multi) { return ( ({ label: val }))} onCreateOption={(newVal: any) => { onChange([...value, newVal]); @@ -29,10 +37,11 @@ export const DatasourceInputVarField: React.FunctionComponent<{ onChange={(newVals: any[]) => { onChange(newVals.map(val => val.label)); }} + onBlur={() => setIsDirty(true)} /> ); } - if (varDef.type === 'yaml') { + if (type === 'yaml') { return ( onChange(newVal)} + onBlur={() => setIsDirty(true)} /> ); } return ( onChange(e.target.value)} + onBlur={() => setIsDirty(true)} /> ); }; return ( ) : null } - helpText={} + helpText={} > {renderField()} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts index e5f18e1449d1b..3bfca75668911 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts @@ -5,3 +5,4 @@ */ export { CreateDatasourcePageLayout } from './layout'; export { DatasourceInputPanel } from './datasource_input_panel'; +export { DatasourceInputVarField } from './datasource_input_var_field'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 23d0f3317a667..7815ab9cd1d6e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -21,6 +21,7 @@ import { useLinks as useEPMLinks } from '../../epm/hooks'; import { CreateDatasourcePageLayout } from './components'; import { CreateDatasourceFrom, CreateDatasourceStep } from './types'; import { CREATE_DATASOURCE_STEP_PATHS } from './constants'; +import { DatasourceValidationResults, validateDatasource } from './services'; import { StepSelectPackage } from './step_select_package'; import { StepSelectConfig } from './step_select_config'; import { StepConfigureDatasource } from './step_configure_datasource'; @@ -51,6 +52,9 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { inputs: [], }); + // Datasource validation state + const [validationResults, setValidationResults] = useState(); + // Update package info method const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { if (updatedPackageInfo) { @@ -84,9 +88,18 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ...updatedFields, }; setDatasource(newDatasource); - // eslint-disable-next-line no-console console.debug('Datasource updated', newDatasource); + updateDatasourceValidation(newDatasource); + }; + + const updateDatasourceValidation = (newDatasource?: NewDatasource) => { + if (packageInfo) { + const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo); + setValidationResults(newValidationResult); + // eslint-disable-next-line no-console + console.debug('Datasource validation results', newValidationResult); + } }; // Cancel url @@ -202,6 +215,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} + validationResults={validationResults!} backLink={ {from === 'config' ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts index 44e5bfa41cb9b..d99f0712db3c3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts @@ -4,3 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ export { isAdvancedVar } from './is_advanced_var'; +export { + DatasourceValidationResults, + DatasourceConfigValidationResults, + DatasourceInputValidationResults, + validateDatasource, + validationHasErrors, +} from './validate_datasource'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts new file mode 100644 index 0000000000000..a45fabeb5ed6a --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts @@ -0,0 +1,504 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + PackageInfo, + InstallationStatus, + NewDatasource, + RegistryDatasource, +} from '../../../../types'; +import { validateDatasource, validationHasErrors } from './validate_datasource'; + +describe('Ingest Manager - validateDatasource()', () => { + const mockPackage = ({ + name: 'mock-package', + title: 'Mock package', + version: '0.0.0', + description: 'description', + type: 'mock', + categories: [], + requirement: { kibana: { versions: '' }, elasticsearch: { versions: '' } }, + format_version: '', + download: '', + path: '', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + 'index-pattern': [], + }, + }, + status: InstallationStatus.notInstalled, + datasources: [ + { + name: 'datasource1', + title: 'Datasource 1', + description: 'test datasource', + inputs: [ + { + type: 'foo', + title: 'Foo', + vars: [ + { default: 'foo-input-var-value', name: 'foo-input-var-name', type: 'text' }, + { + default: 'foo-input2-var-value', + name: 'foo-input2-var-name', + required: true, + type: 'text', + }, + { name: 'foo-input3-var-name', type: 'text', required: true, multi: true }, + ], + streams: [ + { + dataset: 'foo', + input: 'foo', + title: 'Foo', + vars: [{ name: 'var-name', type: 'yaml' }], + }, + ], + }, + { + type: 'bar', + title: 'Bar', + vars: [ + { + default: ['value1', 'value2'], + name: 'bar-input-var-name', + type: 'text', + multi: true, + }, + { name: 'bar-input2-var-name', required: true, type: 'text' }, + ], + streams: [ + { + dataset: 'bar', + input: 'bar', + title: 'Bar', + vars: [{ name: 'var-name', type: 'yaml', required: true }], + }, + { + dataset: 'bar2', + input: 'bar2', + title: 'Bar 2', + vars: [{ default: 'bar2-var-value', name: 'var-name', type: 'text' }], + }, + ], + }, + { + type: 'with-no-config-or-streams', + title: 'With no config or streams', + streams: [], + }, + { + type: 'with-disabled-streams', + title: 'With disabled streams', + streams: [ + { + dataset: 'disabled', + input: 'disabled', + title: 'Disabled', + enabled: false, + vars: [{ multi: true, required: true, name: 'var-name', type: 'text' }], + }, + { dataset: 'disabled2', input: 'disabled2', title: 'Disabled 2', enabled: false }, + ], + }, + ], + }, + ], + } as unknown) as PackageInfo; + + const validDatasource: NewDatasource = { + name: 'datasource1-1', + config_id: 'test-config', + enabled: true, + output_id: 'test-output', + inputs: [ + { + type: 'foo', + enabled: true, + config: { + 'foo-input-var-name': { value: 'foo-input-var-value', type: 'text' }, + 'foo-input2-var-name': { value: 'foo-input2-var-value', type: 'text' }, + 'foo-input3-var-name': { value: ['test'], type: 'text' }, + }, + streams: [ + { + id: 'foo-foo', + dataset: 'foo', + enabled: true, + config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + }, + ], + }, + { + type: 'bar', + enabled: true, + config: { + 'bar-input-var-name': { value: ['value1', 'value2'], type: 'text' }, + 'bar-input2-var-name': { value: 'test', type: 'text' }, + }, + streams: [ + { + id: 'bar-bar', + dataset: 'bar', + enabled: true, + config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + }, + { + id: 'bar-bar2', + dataset: 'bar2', + enabled: true, + config: { 'var-name': { value: undefined, type: 'text' } }, + }, + ], + }, + { + type: 'with-no-config-or-streams', + enabled: true, + streams: [], + }, + { + type: 'with-disabled-streams', + enabled: true, + streams: [ + { + id: 'with-disabled-streams-disabled', + dataset: 'disabled', + enabled: false, + config: { 'var-name': { value: undefined, type: 'text' } }, + }, + { + id: 'with-disabled-streams-disabled2', + dataset: 'disabled2', + enabled: false, + }, + ], + }, + ], + }; + + const invalidDatasource: NewDatasource = { + ...validDatasource, + name: '', + inputs: [ + { + type: 'foo', + enabled: true, + config: { + 'foo-input-var-name': { value: undefined, type: 'text' }, + 'foo-input2-var-name': { value: '', type: 'text' }, + 'foo-input3-var-name': { value: [], type: 'text' }, + }, + streams: [ + { + id: 'foo-foo', + dataset: 'foo', + enabled: true, + config: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, + }, + ], + }, + { + type: 'bar', + enabled: true, + config: { + 'bar-input-var-name': { value: 'invalid value for multi', type: 'text' }, + 'bar-input2-var-name': { value: undefined, type: 'text' }, + }, + streams: [ + { + id: 'bar-bar', + dataset: 'bar', + enabled: true, + config: { 'var-name': { value: ' \n\n', type: 'yaml' } }, + }, + { + id: 'bar-bar2', + dataset: 'bar2', + enabled: true, + config: { 'var-name': { value: undefined, type: 'text' } }, + }, + ], + }, + { + type: 'with-no-config-or-streams', + enabled: true, + streams: [], + }, + { + type: 'with-disabled-streams', + enabled: true, + streams: [ + { + id: 'with-disabled-streams-disabled', + dataset: 'disabled', + enabled: false, + config: { + 'var-name': { + value: 'invalid value but not checked due to not enabled', + type: 'text', + }, + }, + }, + { + id: 'with-disabled-streams-disabled2', + dataset: 'disabled2', + enabled: false, + }, + ], + }, + ], + }; + + const noErrorsValidationResults = { + name: null, + description: null, + inputs: { + foo: { + config: { + 'foo-input-var-name': null, + 'foo-input2-var-name': null, + 'foo-input3-var-name': null, + }, + streams: { 'foo-foo': { config: { 'var-name': null } } }, + }, + bar: { + config: { 'bar-input-var-name': null, 'bar-input2-var-name': null }, + streams: { + 'bar-bar': { config: { 'var-name': null } }, + 'bar-bar2': { config: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + }, + }, + }; + + it('returns no errors for valid datasource configuration', () => { + expect(validateDatasource(validDatasource, mockPackage)).toEqual(noErrorsValidationResults); + }); + + it('returns errors for invalid datasource configuration', () => { + expect(validateDatasource(invalidDatasource, mockPackage)).toEqual({ + name: ['Name is required'], + description: null, + inputs: { + foo: { + config: { + 'foo-input-var-name': null, + 'foo-input2-var-name': ['foo-input2-var-name is required'], + 'foo-input3-var-name': ['foo-input3-var-name is required'], + }, + streams: { 'foo-foo': { config: { 'var-name': ['Invalid YAML format'] } } }, + }, + bar: { + config: { + 'bar-input-var-name': ['Invalid format'], + 'bar-input2-var-name': ['bar-input2-var-name is required'], + }, + streams: { + 'bar-bar': { config: { 'var-name': ['var-name is required'] } }, + 'bar-bar2': { config: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + }, + }, + }); + }); + + it('returns no errors for disabled inputs', () => { + const disabledInputs = invalidDatasource.inputs.map(input => ({ ...input, enabled: false })); + expect(validateDatasource({ ...validDatasource, inputs: disabledInputs }, mockPackage)).toEqual( + noErrorsValidationResults + ); + }); + + it('returns only datasource and input-level errors for disabled streams', () => { + const inputsWithDisabledStreams = invalidDatasource.inputs.map(input => + input.streams + ? { + ...input, + streams: input.streams.map(stream => ({ ...stream, enabled: false })), + } + : input + ); + expect( + validateDatasource({ ...invalidDatasource, inputs: inputsWithDisabledStreams }, mockPackage) + ).toEqual({ + name: ['Name is required'], + description: null, + inputs: { + foo: { + config: { + 'foo-input-var-name': null, + 'foo-input2-var-name': ['foo-input2-var-name is required'], + 'foo-input3-var-name': ['foo-input3-var-name is required'], + }, + streams: { 'foo-foo': { config: { 'var-name': null } } }, + }, + bar: { + config: { + 'bar-input-var-name': ['Invalid format'], + 'bar-input2-var-name': ['bar-input2-var-name is required'], + }, + streams: { + 'bar-bar': { config: { 'var-name': null } }, + 'bar-bar2': { config: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + }, + }, + }); + }); + + it('returns no errors for packages with no datasources', () => { + expect( + validateDatasource(validDatasource, { + ...mockPackage, + datasources: undefined, + }) + ).toEqual({ + name: null, + description: null, + inputs: null, + }); + expect( + validateDatasource(validDatasource, { + ...mockPackage, + datasources: [], + }) + ).toEqual({ + name: null, + description: null, + inputs: null, + }); + }); + + it('returns no errors for packages with no inputs', () => { + expect( + validateDatasource(validDatasource, { + ...mockPackage, + datasources: [{} as RegistryDatasource], + }) + ).toEqual({ + name: null, + description: null, + inputs: null, + }); + expect( + validateDatasource(validDatasource, { + ...mockPackage, + datasources: [({ inputs: [] } as unknown) as RegistryDatasource], + }) + ).toEqual({ + name: null, + description: null, + inputs: null, + }); + }); +}); + +describe('Ingest Manager - validationHasErrors()', () => { + it('returns true for stream validation results with errors', () => { + expect( + validationHasErrors({ + config: { foo: ['foo error'], bar: null }, + }) + ).toBe(true); + }); + + it('returns false for stream validation results with no errors', () => { + expect( + validationHasErrors({ + config: { foo: null, bar: null }, + }) + ).toBe(false); + }); + + it('returns true for input validation results with errors', () => { + expect( + validationHasErrors({ + config: { foo: ['foo error'], bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }) + ).toBe(true); + expect( + validationHasErrors({ + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: ['foo error'], bar: null } } }, + }) + ).toBe(true); + }); + + it('returns false for input validation results with no errors', () => { + expect( + validationHasErrors({ + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }) + ).toBe(false); + }); + + it('returns true for datasource validation results with errors', () => { + expect( + validationHasErrors({ + name: ['name error'], + description: null, + inputs: { + input1: { + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(true); + expect( + validationHasErrors({ + name: null, + description: null, + inputs: { + input1: { + config: { foo: ['foo error'], bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(true); + expect( + validationHasErrors({ + name: null, + description: null, + inputs: { + input1: { + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: ['foo error'], bar: null } } }, + }, + }, + }) + ).toBe(true); + }); + + it('returns false for datasource validation results with no errors', () => { + expect( + validationHasErrors({ + name: null, + description: null, + inputs: { + input1: { + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts new file mode 100644 index 0000000000000..518e2bfc1af07 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { safeLoad } from 'js-yaml'; +import { getFlattenedObject } from '../../../../services'; +import { + NewDatasource, + DatasourceInput, + DatasourceInputStream, + DatasourceConfigRecordEntry, + PackageInfo, + RegistryInput, + RegistryVarsEntry, +} from '../../../../types'; + +type Errors = string[] | null; + +type ValidationEntry = Record; + +export interface DatasourceConfigValidationResults { + config?: ValidationEntry; +} + +export type DatasourceInputValidationResults = DatasourceConfigValidationResults & { + streams?: Record; +}; + +export interface DatasourceValidationResults { + name: Errors; + description: Errors; + inputs: Record | null; +} + +/* + * Returns validation information for a given datasource configuration and package info + * Note: this method assumes that `datasource` is correctly structured for the given package + */ +export const validateDatasource = ( + datasource: NewDatasource, + packageInfo: PackageInfo +): DatasourceValidationResults => { + const validationResults: DatasourceValidationResults = { + name: null, + description: null, + inputs: {}, + }; + + if (!datasource.name.trim()) { + validationResults.name = [ + i18n.translate('xpack.ingestManager.datasourceValidation.nameRequiredErrorMessage', { + defaultMessage: 'Name is required', + }), + ]; + } + + if ( + !packageInfo.datasources || + packageInfo.datasources.length === 0 || + !packageInfo.datasources[0] || + !packageInfo.datasources[0].inputs || + packageInfo.datasources[0].inputs.length === 0 + ) { + validationResults.inputs = null; + return validationResults; + } + + const registryInputsByType: Record< + string, + RegistryInput + > = packageInfo.datasources[0].inputs.reduce((inputs, registryInput) => { + inputs[registryInput.type] = registryInput; + return inputs; + }, {} as Record); + + // Validate each datasource input with either its own config fields or streams + datasource.inputs.forEach(input => { + if (!input.config && !input.streams) { + return; + } + + const inputValidationResults: DatasourceInputValidationResults = { + config: undefined, + streams: {}, + }; + + const inputVarsByName = (registryInputsByType[input.type].vars || []).reduce( + (vars, registryVar) => { + vars[registryVar.name] = registryVar; + return vars; + }, + {} as Record + ); + + // Validate input-level config fields + const inputConfigs = Object.entries(input.config || {}); + if (inputConfigs.length) { + inputValidationResults.config = inputConfigs.reduce((results, [name, configEntry]) => { + results[name] = input.enabled + ? validateDatasourceConfig(configEntry, inputVarsByName[name]) + : null; + return results; + }, {} as ValidationEntry); + } else { + delete inputValidationResults.config; + } + + // Validate each input stream with config fields + if (input.streams.length) { + input.streams.forEach(stream => { + if (!stream.config) { + return; + } + + const streamValidationResults: DatasourceConfigValidationResults = { + config: undefined, + }; + + const streamVarsByName = ( + ( + registryInputsByType[input.type].streams.find( + registryStream => registryStream.dataset === stream.dataset + ) || {} + ).vars || [] + ).reduce((vars, registryVar) => { + vars[registryVar.name] = registryVar; + return vars; + }, {} as Record); + + // Validate stream-level config fields + streamValidationResults.config = Object.entries(stream.config).reduce( + (results, [name, configEntry]) => { + results[name] = + input.enabled && stream.enabled + ? validateDatasourceConfig(configEntry, streamVarsByName[name]) + : null; + return results; + }, + {} as ValidationEntry + ); + + inputValidationResults.streams![stream.id] = streamValidationResults; + }); + } else { + delete inputValidationResults.streams; + } + + if (inputValidationResults.config || inputValidationResults.streams) { + validationResults.inputs![input.type] = inputValidationResults; + } + }); + + if (Object.entries(validationResults.inputs!).length === 0) { + validationResults.inputs = null; + } + return validationResults; +}; + +const validateDatasourceConfig = ( + configEntry: DatasourceConfigRecordEntry, + varDef: RegistryVarsEntry +): string[] | null => { + const errors = []; + const { value } = configEntry; + let parsedValue: any = value; + + if (typeof value === 'string') { + parsedValue = value.trim(); + } + + if (varDef.required) { + if (parsedValue === undefined || (typeof parsedValue === 'string' && !parsedValue)) { + errors.push( + i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', { + defaultMessage: '{fieldName} is required', + values: { + fieldName: varDef.title || varDef.name, + }, + }) + ); + } + } + + if (varDef.type === 'yaml') { + try { + parsedValue = safeLoad(value); + } catch (e) { + errors.push( + i18n.translate('xpack.ingestManager.datasourceValidation.invalidYamlFormatErrorMessage', { + defaultMessage: 'Invalid YAML format', + }) + ); + } + } + + if (varDef.multi) { + if (parsedValue && !Array.isArray(parsedValue)) { + errors.push( + i18n.translate('xpack.ingestManager.datasourceValidation.invalidArrayErrorMessage', { + defaultMessage: 'Invalid format', + }) + ); + } + if ( + varDef.required && + (!parsedValue || (Array.isArray(parsedValue) && parsedValue.length === 0)) + ) { + errors.push( + i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', { + defaultMessage: '{fieldName} is required', + values: { + fieldName: varDef.title || varDef.name, + }, + }) + ); + } + } + + return errors.length ? errors : null; +}; + +export const validationHasErrors = ( + validationResults: + | DatasourceValidationResults + | DatasourceInputValidationResults + | DatasourceConfigValidationResults +) => { + const flattenedValidation = getFlattenedObject(validationResults); + return !!Object.entries(flattenedValidation).find(([, value]) => !!value); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx index b45beef4a8b5e..105d6c66a5704 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx @@ -9,17 +9,16 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSteps, EuiPanel, - EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiFieldText, EuiButtonEmpty, EuiSpacer, EuiEmptyPrompt, EuiText, EuiButton, EuiComboBox, + EuiCallOut, } from '@elastic/eui'; import { AgentConfig, @@ -28,21 +27,37 @@ import { NewDatasource, DatasourceInput, } from '../../../types'; +import { Loading } from '../../../components'; import { packageToConfigDatasourceInputs } from '../../../services'; -import { DatasourceInputPanel } from './components'; +import { DatasourceValidationResults, validationHasErrors } from './services'; +import { DatasourceInputPanel, DatasourceInputVarField } from './components'; export const StepConfigureDatasource: React.FunctionComponent<{ agentConfig: AgentConfig; packageInfo: PackageInfo; datasource: NewDatasource; updateDatasource: (fields: Partial) => void; + validationResults: DatasourceValidationResults; backLink: JSX.Element; cancelUrl: string; onNext: () => void; -}> = ({ agentConfig, packageInfo, datasource, updateDatasource, backLink, cancelUrl, onNext }) => { +}> = ({ + agentConfig, + packageInfo, + datasource, + updateDatasource, + validationResults, + backLink, + cancelUrl, + onNext, +}) => { // Form show/hide states const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false); + // Form submit state + const [submitAttempted, setSubmitAttempted] = useState(false); + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + // Update datasource's package and config info useEffect(() => { const dsPackage = datasource.package; @@ -81,56 +96,56 @@ export const StepConfigureDatasource: React.FunctionComponent<{ }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); // Step A, define datasource - const DefineDatasource = ( + const renderDefineDatasource = () => ( - - - - } - > - - updateDatasource({ - name: e.target.value, - }) - } - /> - + + + { + updateDatasource({ + name: newValue, + }); + }} + errors={validationResults!.name} + forceShowErrors={submitAttempted} + /> - - - } - labelAppend={ - - - - } - > - - updateDatasource({ - description: e.target.value, - }) - } - /> - + + { + updateDatasource({ + description: newValue, + }); + }} + errors={validationResults!.description} + forceShowErrors={submitAttempted} + /> - + - - + + - + + ) : null} @@ -182,7 +198,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{ // Step B, configure inputs (and their streams) // Assume packages only export one datasource for now - const ConfigureInputs = + const renderConfigureInputs = () => packageInfo.datasources && packageInfo.datasources[0] && packageInfo.datasources[0].inputs && @@ -208,6 +224,8 @@ export const StepConfigureDatasource: React.FunctionComponent<{ inputs: newInputs, }); }} + inputValidationResults={validationResults!.inputs![datasourceInput.type]} + forceShowErrors={submitAttempted} /> ) : null; @@ -232,7 +250,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{
); - return ( + return validationResults ? ( @@ -251,7 +269,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{ defaultMessage: 'Define your datasource', } ), - children: DefineDatasource, + children: renderDefineDatasource(), }, { title: i18n.translate( @@ -260,13 +278,34 @@ export const StepConfigureDatasource: React.FunctionComponent<{ defaultMessage: 'Choose the data you want to collect', } ), - children: ConfigureInputs, + children: renderConfigureInputs(), }, ]} /> + {hasErrors && submitAttempted ? ( + + +

+ +

+
+ +
+ ) : null} @@ -278,7 +317,17 @@ export const StepConfigureDatasource: React.FunctionComponent<{ - onNext()}> + { + setSubmitAttempted(true); + if (!hasErrors) { + onNext(); + } + }} + > + ) : ( + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 0aa08602e4d4d..5ebd1300baf65 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export { getFlattenedObject } from '../../../../../../../src/core/utils'; + export { agentConfigRouteService, datasourceRouteService, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 333a9b049fa85..32615278b67d7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -16,6 +16,7 @@ export { NewDatasource, DatasourceInput, DatasourceInputStream, + DatasourceConfigRecordEntry, // API schemas - Agent Config GetAgentConfigsResponse, GetAgentConfigsResponseItem, @@ -56,6 +57,7 @@ export { RegistryVarsEntry, RegistryInput, RegistryStream, + RegistryDatasource, PackageList, PackageListItem, PackagesGroupedByStatus, @@ -70,4 +72,5 @@ export { DeletePackageResponse, DetailViewPanelName, InstallStatus, + InstallationStatus, } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts index aa1e0e79e548b..c11ad60dffee4 100644 --- a/x-pack/plugins/ingest_manager/public/index.ts +++ b/x-pack/plugins/ingest_manager/public/index.ts @@ -6,6 +6,8 @@ import { PluginInitializerContext } from 'src/core/public'; import { IngestManagerPlugin } from './plugin'; +export { IngestManagerStart } from './plugin'; + export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index d7be1c1f1fe6e..77bba0bb0f990 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -17,11 +17,20 @@ import { LicensingPluginSetup } from '../../licensing/public'; import { PLUGIN_ID } from '../common/constants'; import { IngestManagerConfigType } from '../common/types'; +import { setupRouteService } from '../common'; export { IngestManagerConfigType } from '../common/types'; export type IngestManagerSetup = void; -export type IngestManagerStart = void; +/** + * Describes public IngestManager plugin contract returned at the `start` stage. + */ +export interface IngestManagerStart { + success: boolean; + error?: { + message: string; + }; +} export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; @@ -61,7 +70,14 @@ export class IngestManagerPlugin }); } - public start(core: CoreStart) {} + public async start(core: CoreStart): Promise { + try { + const { isInitialized: success } = await core.http.post(setupRouteService.getSetupPath()); + return { success }; + } catch (error) { + return { success: false, error: { message: error.body?.message || 'Unknown error' } }; + } + } public stop() {} } diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 48f37a4d65ac6..ad16e1dde456b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -102,7 +102,9 @@ export const getInfoHandler: RequestHandler - storedDatasourceToAgentDatasource(ds) - ), + datasources: (config.datasources as Datasource[]) + .filter(datasource => datasource.enabled) + .map(ds => storedDatasourceToAgentDatasource(ds)), revision: config.revision, }; diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index 8fa1428f3a055..1b8f2a690b94d 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -9,7 +9,7 @@ import { DeleteDatasourcesResponse, packageToConfigDatasource } from '../../comm import { DATASOURCE_SAVED_OBJECT_TYPE } from '../constants'; import { NewDatasource, Datasource, ListWithKuery } from '../types'; import { agentConfigService } from './agent_config'; -import { findInstalledPackageByName, getPackageInfo } from './epm/packages'; +import { getPackageInfo, getInstallation } from './epm/packages'; import { outputService } from './output'; const SAVED_OBJECT_TYPE = DATASOURCE_SAVED_OBJECT_TYPE; @@ -172,15 +172,13 @@ class DatasourceService { soClient: SavedObjectsClientContract, pkgName: string ): Promise { - const pkgInstall = await findInstalledPackageByName({ - savedObjectsClient: soClient, - pkgName, - }); + const pkgInstall = await getInstallation({ savedObjectsClient: soClient, pkgName }); if (pkgInstall) { const [pkgInfo, defaultOutputId] = await Promise.all([ getPackageInfo({ savedObjectsClient: soClient, - pkgkey: `${pkgInstall.name}-${pkgInstall.version}`, + pkgName: pkgInstall.name, + pkgVersion: pkgInstall.version, }), outputService.getDefaultOutputId(soClient), ]); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts index c56322239f27b..60a85e367079f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts @@ -7,9 +7,15 @@ import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; import * as Registry from '../../registry'; -export async function installILMPolicy(pkgkey: string, callCluster: CallESAsCurrentUser) { - const ilmPaths = await Registry.getArchiveInfo(pkgkey, (entry: Registry.ArchiveEntry) => - isILMPolicy(entry) +export async function installILMPolicy( + pkgName: string, + pkgVersion: string, + callCluster: CallESAsCurrentUser +) { + const ilmPaths = await Registry.getArchiveInfo( + pkgName, + pkgVersion, + (entry: Registry.ArchiveEntry) => isILMPolicy(entry) ); if (!ilmPaths.length) return; await Promise.all( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 4b65e5554567e..2bbb555ef7393 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -30,11 +30,10 @@ export const installPipelines = async ( if (dataset.ingest_pipeline) { acc.push( installPipelinesForDataset({ - pkgkey: Registry.pkgToPkgKey(registryPackage), dataset, callCluster, - packageName: registryPackage.name, - packageVersion: registryPackage.version, + pkgName: registryPackage.name, + pkgVersion: registryPackage.version, }) ); } @@ -68,19 +67,19 @@ export function rewriteIngestPipeline( export async function installPipelinesForDataset({ callCluster, - pkgkey, + pkgName, + pkgVersion, dataset, - packageName, - packageVersion, }: { callCluster: CallESAsCurrentUser; - pkgkey: string; + pkgName: string; + pkgVersion: string; dataset: Dataset; - packageName: string; - packageVersion: string; }): Promise { - const pipelinePaths = await Registry.getArchiveInfo(pkgkey, (entry: Registry.ArchiveEntry) => - isDatasetPipeline(entry, dataset.path) + const pipelinePaths = await Registry.getArchiveInfo( + pkgName, + pkgVersion, + (entry: Registry.ArchiveEntry) => isDatasetPipeline(entry, dataset.path) ); let pipelines: any[] = []; const substitutions: RewriteSubstitution[] = []; @@ -90,7 +89,7 @@ export async function installPipelinesForDataset({ const nameForInstallation = getPipelineNameForInstallation({ pipelineName: name, dataset, - packageVersion, + packageVersion: pkgVersion, }); const content = Registry.getAsset(path).toString('utf-8'); pipelines.push({ diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index de4ba25590c98..560ddfc1f6885 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -20,11 +20,12 @@ import * as Registry from '../../registry'; export const installTemplates = async ( registryPackage: RegistryPackage, callCluster: CallESAsCurrentUser, - pkgkey: string + pkgName: string, + pkgVersion: string ) => { // install any pre-built index template assets, // atm, this is only the base package's global template - installPreBuiltTemplates(pkgkey, callCluster); + installPreBuiltTemplates(pkgName, pkgVersion, callCluster); // build templates per dataset from yml files const datasets = registryPackage.datasets; @@ -45,9 +46,15 @@ export const installTemplates = async ( }; // this is temporary until we update the registry to use index templates v2 structure -const installPreBuiltTemplates = async (pkgkey: string, callCluster: CallESAsCurrentUser) => { - const templatePaths = await Registry.getArchiveInfo(pkgkey, (entry: Registry.ArchiveEntry) => - isTemplate(entry) +const installPreBuiltTemplates = async ( + pkgName: string, + pkgVersion: string, + callCluster: CallESAsCurrentUser +) => { + const templatePaths = await Registry.getArchiveInfo( + pkgName, + pkgVersion, + (entry: Registry.ArchiveEntry) => isTemplate(entry) ); templatePaths.forEach(async path => { const { file } = Registry.pathParts(path); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 0657fb7759b49..05e64c6565dc6 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -68,25 +68,32 @@ export enum IndexPatternType { metrics = 'metrics', events = 'events', } - +// TODO: use a function overload and make pkgName and pkgVersion required for install/update +// and not for an update removal. or separate out the functions export async function installIndexPatterns( savedObjectsClient: SavedObjectsClientContract, - pkgkey?: string + pkgName?: string, + pkgVersion?: string ) { // get all user installed packages const installedPackages = await getPackageKeysByStatus( savedObjectsClient, InstallationStatus.installed ); - // add this package to the array if it doesn't already exist - // this should not happen because a user can't "reinstall" a package - // if it does because the install endpoint is called directly, the install continues - if (pkgkey && !installedPackages.includes(pkgkey)) { - installedPackages.push(pkgkey); + if (pkgName && pkgVersion) { + // add this package to the array if it doesn't already exist + const foundPkg = installedPackages.find(pkg => pkg.pkgName === pkgName); + // this may be removed if we add the packged to saved objects before installing index patterns + // otherwise this is a first time install + // TODO: handle update case when versions are different + if (!foundPkg) { + installedPackages.push({ pkgName, pkgVersion }); + } } - // get each package's registry info - const installedPackagesFetchInfoPromise = installedPackages.map(pkg => Registry.fetchInfo(pkg)); + const installedPackagesFetchInfoPromise = installedPackages.map(pkg => + Registry.fetchInfo(pkg.pkgName, pkg.pkgVersion) + ); const installedPackagesInfo = await Promise.all(installedPackagesFetchInfoPromise); // for each index pattern type, create an index pattern @@ -97,7 +104,7 @@ export async function installIndexPatterns( ]; indexPatternTypes.forEach(async indexPatternType => { // if this is an update because a package is being unisntalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern - if (!pkgkey && installedPackages.length === 0) { + if (!pkgName && installedPackages.length === 0) { try { await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, `${indexPatternType}-*`); } catch (err) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index d7a5c5569986e..7026d9eae24c3 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -58,7 +58,7 @@ export async function getAssetsData( ): Promise { // TODO: Needs to be called to fill the cache but should not be required const pkgkey = packageInfo.name + '-' + packageInfo.version; - if (!cacheHas(pkgkey)) await Registry.getArchiveInfo(pkgkey); + if (!cacheHas(pkgkey)) await Registry.getArchiveInfo(packageInfo.name, packageInfo.version); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index e963ea138dfd5..0e2c2a3d26073 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -41,7 +41,7 @@ export async function getPackages( .map(item => createInstallableFrom( item, - savedObjectsVisible.find(({ attributes }) => attributes.name === item.name) + savedObjectsVisible.find(({ id }) => id === item.name) ) ) .sort(sortByName); @@ -53,9 +53,9 @@ export async function getPackageKeysByStatus( status: InstallationStatus ) { const allPackages = await getPackages({ savedObjectsClient }); - return allPackages.reduce((acc, pkg) => { + return allPackages.reduce>((acc, pkg) => { if (pkg.status === status) { - acc.push(`${pkg.name}-${pkg.version}`); + acc.push({ pkgName: pkg.name, pkgVersion: pkg.version }); } return acc; }, []); @@ -63,13 +63,14 @@ export async function getPackageKeysByStatus( export async function getPackageInfo(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; + pkgVersion: string; }): Promise { - const { savedObjectsClient, pkgkey } = options; + const { savedObjectsClient, pkgName, pkgVersion } = options; const [item, savedObject] = await Promise.all([ - Registry.fetchInfo(pkgkey), - getInstallationObject({ savedObjectsClient, pkgkey }), - Registry.getArchiveInfo(pkgkey), + Registry.fetchInfo(pkgName, pkgVersion), + getInstallationObject({ savedObjectsClient, pkgName }), + Registry.getArchiveInfo(pkgName, pkgVersion), ] as const); // adding `as const` due to regression in TS 3.7.2 // see https://github.com/microsoft/TypeScript/issues/34925#issuecomment-550021453 @@ -86,37 +87,22 @@ export async function getPackageInfo(options: { export async function getInstallationObject(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; }) { - const { savedObjectsClient, pkgkey } = options; + const { savedObjectsClient, pkgName } = options; return savedObjectsClient - .get(PACKAGES_SAVED_OBJECT_TYPE, pkgkey) + .get(PACKAGES_SAVED_OBJECT_TYPE, pkgName) .catch(e => undefined); } export async function getInstallation(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; }) { const savedObject = await getInstallationObject(options); return savedObject?.attributes; } -export async function findInstalledPackageByName(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; -}): Promise { - const { savedObjectsClient, pkgName } = options; - - const res = await savedObjectsClient.find({ - type: PACKAGES_SAVED_OBJECT_TYPE, - search: pkgName, - searchFields: ['name'], - }); - if (res.saved_objects.length) return res.saved_objects[0].attributes; - return undefined; -} - function sortByName(a: { name: string }, b: { name: string }) { if (a.name > b.name) { return 1; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts index b924c045870f3..b623295c5e060 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts @@ -11,46 +11,6 @@ import * as Registry from '../registry'; type ArchiveAsset = Pick; type SavedObjectToBe = Required & { type: AssetType }; -export async function getObjects( - pkgkey: string, - filter = (entry: Registry.ArchiveEntry): boolean => true -): Promise { - // Create a Map b/c some values, especially index-patterns, are referenced multiple times - const objects: Map = new Map(); - - // Get paths which match the given filter - const paths = await Registry.getArchiveInfo(pkgkey, filter); - - // Get all objects which matched filter. Add them to the Map - const rootObjects = await Promise.all(paths.map(getObject)); - rootObjects.forEach(obj => objects.set(obj.id, obj)); - - // Each of those objects might have `references` property like [{id, type, name}] - for (const object of rootObjects) { - // For each of those objects, if they have references - for (const reference of object.references) { - // Get the referenced objects. Call same function with a new filter - const referencedObjects = await getObjects(pkgkey, (entry: Registry.ArchiveEntry) => { - // Skip anything we've already stored - if (objects.has(reference.id)) return false; - - // Is the archive entry the reference we want? - const { type, file } = Registry.pathParts(entry.path); - const isType = type === reference.type; - const isJson = file === `${reference.id}.json`; - - return isType && isJson; - }); - - // Add referenced objects to the Map - referencedObjects.forEach(ro => objects.set(ro.id, ro)); - } - } - - // return the array of unique objects - return Array.from(objects.values()); -} - export async function getObject(key: string) { const buffer = Registry.getAsset(key); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 79259ce79ff41..d49e0e661440f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -21,7 +21,6 @@ export { getPackageInfo, getPackages, SearchParams, - findInstalledPackageByName, } from './get'; export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 82523e37509d1..e250b4f176819 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -16,7 +16,7 @@ import { import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; import { getObject } from './get_objects'; -import { getInstallation, findInstalledPackageByName } from './index'; +import { getInstallation } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; @@ -63,7 +63,7 @@ export async function ensureInstalledPackage(options: { callCluster: CallESAsCurrentUser; }): Promise { const { savedObjectsClient, pkgName, callCluster } = options; - const installedPackage = await findInstalledPackageByName({ savedObjectsClient, pkgName }); + const installedPackage = await getInstallation({ savedObjectsClient, pkgName }); if (installedPackage) { return installedPackage; } @@ -74,7 +74,7 @@ export async function ensureInstalledPackage(options: { pkgName, callCluster, }); - return await findInstalledPackageByName({ savedObjectsClient, pkgName }); + return await getInstallation({ savedObjectsClient, pkgName }); } catch (err) { throw new Error(err.message); } @@ -86,22 +86,30 @@ export async function installPackage(options: { callCluster: CallESAsCurrentUser; }): Promise { const { savedObjectsClient, pkgkey, callCluster } = options; - const registryPackageInfo = await Registry.fetchInfo(pkgkey); - const { name: pkgName, version: pkgVersion, internal = false } = registryPackageInfo; + // TODO: change epm API to /packageName/version so we don't need to do this + const [pkgName, pkgVersion] = pkgkey.split('-'); + const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); + const { internal = false } = registryPackageInfo; const installKibanaAssetsPromise = installKibanaAssets({ savedObjectsClient, - pkgkey, + pkgName, + pkgVersion, }); const installPipelinePromises = installPipelines(registryPackageInfo, callCluster); - const installTemplatePromises = installTemplates(registryPackageInfo, callCluster, pkgkey); + const installTemplatePromises = installTemplates( + registryPackageInfo, + callCluster, + pkgName, + pkgVersion + ); // index patterns and ilm policies are not currently associated with a particular package // so we do not save them in the package saved object state. at some point ILM policies can be installed/modified // per dataset and we should then save them - await installIndexPatterns(savedObjectsClient, pkgkey); + await installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); // currenly only the base package has an ILM policy - await installILMPolicy(pkgkey, callCluster); + await installILMPolicy(pkgName, pkgVersion, callCluster); const res = await Promise.all([ installKibanaAssetsPromise, @@ -126,14 +134,15 @@ export async function installPackage(options: { // e.g. switch statement with cases for each enum key returning `never` for default case export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; + pkgVersion: string; }) { - const { savedObjectsClient, pkgkey } = options; + const { savedObjectsClient, pkgName, pkgVersion } = options; // Only install Kibana assets during package installation. const kibanaAssetTypes = Object.values(KibanaAssetType); const installationPromises = kibanaAssetTypes.map(async assetType => - installKibanaSavedObjects({ savedObjectsClient, pkgkey, assetType }) + installKibanaSavedObjects({ savedObjectsClient, pkgName, pkgVersion, assetType }) ); // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] @@ -149,8 +158,8 @@ export async function saveInstallationReferences(options: { internal: boolean; toSave: AssetReference[]; }) { - const { savedObjectsClient, pkgkey, pkgName, pkgVersion, internal, toSave } = options; - const installation = await getInstallation({ savedObjectsClient, pkgkey }); + const { savedObjectsClient, pkgName, pkgVersion, internal, toSave } = options; + const installation = await getInstallation({ savedObjectsClient, pkgName }); const savedRefs = installation?.installed || []; const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => { const hasRef = current.find(c => c.id === pending.id && c.type === pending.type); @@ -162,7 +171,7 @@ export async function saveInstallationReferences(options: { await savedObjectsClient.create( PACKAGES_SAVED_OBJECT_TYPE, { installed: toInstall, name: pkgName, version: pkgVersion, internal }, - { id: pkgkey, overwrite: true } + { id: pkgName, overwrite: true } ); return toInstall; @@ -170,16 +179,18 @@ export async function saveInstallationReferences(options: { async function installKibanaSavedObjects({ savedObjectsClient, - pkgkey, + pkgName, + pkgVersion, assetType, }: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; + pkgVersion: string; assetType: KibanaAssetType; }) { const isSameType = ({ path }: Registry.ArchiveEntry) => assetType === Registry.pathParts(path).type; - const paths = await Registry.getArchiveInfo(pkgkey, isSameType); + const paths = await Registry.getArchiveInfo(pkgName, pkgVersion, isSameType); const toBeSavedObjects = await Promise.all(paths.map(getObject)); if (toBeSavedObjects.length === 0) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 2e73160453c2b..a30acb97b99cf 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -17,12 +17,14 @@ export async function removeInstallation(options: { callCluster: CallESAsCurrentUser; }): Promise { const { savedObjectsClient, pkgkey, callCluster } = options; - const installation = await getInstallation({ savedObjectsClient, pkgkey }); + // TODO: the epm api should change to /name/version so we don't need to do this + const [pkgName] = pkgkey.split('-'); + const installation = await getInstallation({ savedObjectsClient, pkgName }); const installedObjects = installation?.installed || []; // Delete the manager saved object with references to the asset objects // could also update with [] or some other state - await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgkey); + await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); // recreate or delete index patterns when a package is uninstalled await installIndexPatterns(savedObjectsClient); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 36a04b88bba29..a96afc5eb7fa5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -56,10 +56,9 @@ export async function fetchFindLatestPackage( } } -export async function fetchInfo(pkgkey: string): Promise { +export async function fetchInfo(pkgName: string, pkgVersion: string): Promise { const registryUrl = appContextService.getConfig()?.epm.registryUrl; - // change pkg-version to pkg/version - return fetchUrl(`${registryUrl}/package/${pkgkey.replace('-', '/')}`).then(JSON.parse); + return fetchUrl(`${registryUrl}/package/${pkgName}/${pkgVersion}`).then(JSON.parse); } export async function fetchFile(filePath: string): Promise { @@ -73,7 +72,8 @@ export async function fetchCategories(): Promise { } export async function getArchiveInfo( - pkgkey: string, + pkgName: string, + pkgVersion: string, filter = (entry: ArchiveEntry): boolean => true ): Promise { const paths: string[] = []; @@ -87,7 +87,7 @@ export async function getArchiveInfo( } }; - await extract(pkgkey, filter, onEntry); + await extract(pkgName, pkgVersion, filter, onEntry); return paths; } @@ -123,21 +123,22 @@ export function pathParts(path: string): AssetParts { } async function extract( - pkgkey: string, + pkgName: string, + pkgVersion: string, filter = (entry: ArchiveEntry): boolean => true, onEntry: (entry: ArchiveEntry) => void ) { - const archiveBuffer = await getOrFetchArchiveBuffer(pkgkey); + const archiveBuffer = await getOrFetchArchiveBuffer(pkgName, pkgVersion); return untarBuffer(archiveBuffer, filter, onEntry); } -async function getOrFetchArchiveBuffer(pkgkey: string): Promise { +async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { // assume .tar.gz for now. add support for .zip if/when we need it - const key = `${pkgkey}.tar.gz`; + const key = `${pkgName}-${pkgVersion}.tar.gz`; let buffer = cacheGet(key); if (!buffer) { - buffer = await fetchArchiveBuffer(pkgkey); + buffer = await fetchArchiveBuffer(pkgName, pkgVersion); cacheSet(key, buffer); } @@ -148,8 +149,8 @@ async function getOrFetchArchiveBuffer(pkgkey: string): Promise { } } -async function fetchArchiveBuffer(key: string): Promise { - const { download: archivePath } = await fetchInfo(key); +async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { + const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); const registryUrl = appContextService.getConfig()?.epm.registryUrl; return getResponseStream(`${registryUrl}${archivePath}`).then(streamToBuffer); } diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 224355ced7cb1..bbaf083fb8396 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -120,7 +120,8 @@ async function addPackageToConfig( ) { const packageInfo = await getPackageInfo({ savedObjectsClient: soClient, - pkgkey: `${packageToInstall.name}-${packageToInstall.version}`, + pkgName: packageToInstall.name, + pkgVersion: packageToInstall.version, }); await datasourceService.create( soClient, diff --git a/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts b/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts index 65f952ca01038..34f7dd4b9578f 100644 --- a/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts @@ -90,7 +90,7 @@ export class ESAggField implements IESAggField { async createTooltipProperty(value: string | undefined): Promise { const indexPattern = await this._source.getIndexPattern(); const tooltipProperty = new TooltipProperty(this.getName(), await this.getLabel(), value); - return new ESAggTooltipProperty(tooltipProperty, indexPattern, this); + return new ESAggTooltipProperty(tooltipProperty, indexPattern, this, this.getAggType()); } getValueAggDsl(indexPattern: IndexPattern): unknown | null { diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts b/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts index 24011c51ddbaa..acd05475f9762 100644 --- a/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts +++ b/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts @@ -4,9 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ import { ESTooltipProperty } from './es_tooltip_property'; +import { AGG_TYPE } from '../../../common/constants'; +import { ITooltipProperty } from './tooltip_property'; +import { IField } from '../fields/field'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; export class ESAggTooltipProperty extends ESTooltipProperty { + private readonly _aggType: AGG_TYPE; + + constructor( + tooltipProperty: ITooltipProperty, + indexPattern: IndexPattern, + field: IField, + aggType: AGG_TYPE + ) { + super(tooltipProperty, indexPattern, field); + this._aggType = aggType; + } + isFilterable(): boolean { - return false; + return this._aggType === AGG_TYPE.TERMS; } } diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts b/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts new file mode 100644 index 0000000000000..2cc9e1513719b --- /dev/null +++ b/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public'; +import { ESTooltipProperty } from './es_tooltip_property'; +import { TooltipProperty } from './tooltip_property'; +import { AbstractField } from '../fields/field'; +import { FIELD_ORIGIN } from '../../../common/constants'; + +class MockField extends AbstractField {} + +const indexPatternField = { + name: 'machine.os', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, +} as IFieldType; + +const featurePropertyField = new MockField({ + fieldName: 'machine.os', + origin: FIELD_ORIGIN.SOURCE, +}); + +const indexPattern = { + id: 'indexPatternId', + fields: { + getByName: (name: string): IFieldType | null => { + return name === 'machine.os' ? indexPatternField : null; + }, + }, + title: 'my index pattern', +} as IndexPattern; + +describe('getESFilters', () => { + test('Should return empty array when field does not exist in index pattern', async () => { + const notFoundFeaturePropertyField = new MockField({ + fieldName: 'field name that is not in index pattern', + origin: FIELD_ORIGIN.SOURCE, + }); + const esTooltipProperty = new ESTooltipProperty( + new TooltipProperty( + notFoundFeaturePropertyField.getName(), + await notFoundFeaturePropertyField.getLabel(), + 'my value' + ), + indexPattern, + notFoundFeaturePropertyField + ); + expect(await esTooltipProperty.getESFilters()).toEqual([]); + }); + + test('Should return phrase filter when field value is provided', async () => { + const esTooltipProperty = new ESTooltipProperty( + new TooltipProperty( + featurePropertyField.getName(), + await featurePropertyField.getLabel(), + 'my value' + ), + indexPattern, + featurePropertyField + ); + expect(await esTooltipProperty.getESFilters()).toEqual([ + { + meta: { + index: 'indexPatternId', + }, + query: { + match_phrase: { + ['machine.os']: 'my value', + }, + }, + }, + ]); + }); + + test('Should return NOT exists filter for null values', async () => { + const esTooltipProperty = new ESTooltipProperty( + new TooltipProperty( + featurePropertyField.getName(), + await featurePropertyField.getLabel(), + undefined + ), + indexPattern, + featurePropertyField + ); + expect(await esTooltipProperty.getESFilters()).toEqual([ + { + meta: { + index: 'indexPatternId', + negate: true, + }, + exists: { + field: 'machine.os', + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts b/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts index 5c35009881920..d2fdcfaab476c 100644 --- a/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts +++ b/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts @@ -7,8 +7,12 @@ import _ from 'lodash'; import { ITooltipProperty } from './tooltip_property'; import { IField } from '../fields/field'; -import { esFilters, IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public'; -import { PhraseFilter } from '../../../../../../src/plugins/data/public'; +import { + esFilters, + Filter, + IFieldType, + IndexPattern, +} from '../../../../../../src/plugins/data/public'; export class ESTooltipProperty implements ITooltipProperty { private readonly _tooltipProperty: ITooltipProperty; @@ -64,12 +68,19 @@ export class ESTooltipProperty implements ITooltipProperty { ); } - async getESFilters(): Promise { + async getESFilters(): Promise { const indexPatternField = this._getIndexPatternField(); if (!indexPatternField) { return []; } - return [esFilters.buildPhraseFilter(indexPatternField, this.getRawValue(), this._indexPattern)]; + const value = this.getRawValue(); + if (value == null) { + const existsFilter = esFilters.buildExistsFilter(indexPatternField, this._indexPattern); + existsFilter.meta.negate = true; + return [existsFilter]; + } else { + return [esFilters.buildPhraseFilter(indexPatternField, value, this._indexPattern)]; + } } } diff --git a/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts b/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts index 4af236f6e9e36..cc95c12ef630f 100644 --- a/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts +++ b/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts @@ -6,7 +6,7 @@ import { ITooltipProperty } from './tooltip_property'; import { IJoin } from '../joins/join'; -import { PhraseFilter } from '../../../../../../src/plugins/data/public'; +import { Filter } from '../../../../../../src/plugins/data/public'; export class JoinTooltipProperty implements ITooltipProperty { private readonly _tooltipProperty: ITooltipProperty; @@ -37,7 +37,7 @@ export class JoinTooltipProperty implements ITooltipProperty { return this._tooltipProperty.getHtmlDisplayValue(); } - async getESFilters(): Promise { + async getESFilters(): Promise { const esFilters = []; if (this._tooltipProperty.isFilterable()) { esFilters.push(...(await this._tooltipProperty.getESFilters())); diff --git a/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts index 7d680dfe9cae0..8da2ed795943b 100644 --- a/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts +++ b/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { PhraseFilter } from '../../../../../../src/plugins/data/public'; +import { Filter } from '../../../../../../src/plugins/data/public'; import { TooltipFeature } from '../../../../../plugins/maps/common/descriptor_types'; export interface ITooltipProperty { @@ -14,7 +14,7 @@ export interface ITooltipProperty { getHtmlDisplayValue(): string; getRawValue(): string | undefined; isFilterable(): boolean; - getESFilters(): Promise; + getESFilters(): Promise; } export interface LoadFeatureProps { @@ -70,7 +70,7 @@ export class TooltipProperty implements ITooltipProperty { return false; } - async getESFilters(): Promise { + async getESFilters(): Promise { return []; } } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index e5f30a50ed8f0..199100d8b5ab0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -55,7 +55,14 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setFormState, setEstimatedModelMemoryLimit } = actions; const mlContext = useMlContext(); - const { form, indexPatternsMap, isAdvancedEditorEnabled, isJobCreated, requestMessages } = state; + const { + estimatedModelMemoryLimit, + form, + indexPatternsMap, + isAdvancedEditorEnabled, + isJobCreated, + requestMessages, + } = state; const forceInput = useRef(null); const firstUpdate = useRef(true); @@ -152,6 +159,9 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const debouncedGetExplainData = debounce(async () => { const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit; + const shouldUpdateEstimatedMml = + !firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === ''; + if (firstUpdate.current) { firstUpdate.current = false; } @@ -167,13 +177,12 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const jobConfig = getJobConfigFromFormState(form); delete jobConfig.dest; delete jobConfig.model_memory_limit; - delete jobConfig.analyzed_fields; const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( jobConfig ); const expectedMemoryWithoutDisk = resp.memory_estimation?.expected_memory_without_disk; - if (shouldUpdateModelMemoryLimit) { + if (shouldUpdateEstimatedMml) { setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); } @@ -250,7 +259,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta dependentVariableOptions: [] as State['form']['dependentVariableOptions'], }; - await newJobCapsService.initializeFromIndexPattern(indexPattern); + await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); // Get fields and filter for supported types for job type const { fields } = newJobCapsService; @@ -340,7 +349,14 @@ export const CreateAnalyticsForm: FC = ({ actions, sta return () => { debouncedGetExplainData.cancel(); }; - }, [jobType, sourceIndex, sourceIndexNameEmpty, dependentVariable, trainingPercent]); + }, [ + jobType, + sourceIndex, + sourceIndexNameEmpty, + dependentVariable, + trainingPercent, + JSON.stringify(excludes), + ]); // Temp effect to close the context menu popover on Clone button click useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts index 9c0bc69f4b41f..4bd03fec7cc72 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts @@ -10,7 +10,7 @@ import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { AnalyticsJobType } from '../../hooks/use_create_analytics_form/state'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields'; -const CATEGORICAL_TYPES = new Set(['ip', 'keyword', 'text']); +const CATEGORICAL_TYPES = new Set(['ip', 'keyword']); // List of system fields we want to ignore for the numeric field check. export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 01a39d2ef9f3b..e121268e65e86 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -8,6 +8,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { isClassificationAnalysis, @@ -158,6 +159,55 @@ export const getInitialState = (): State => ({ estimatedModelMemoryLimit: '', }); +const getExcludesFields = (excluded: string[]) => { + const { fields } = newJobCapsService; + const updatedExcluded: string[] = []; + // Loop through excluded fields to check for multiple types of same field + for (let i = 0; i < excluded.length; i++) { + const fieldName = excluded[i]; + let mainField; + + // No dot in fieldName - it is the main field + if (fieldName.includes('.') === false) { + mainField = fieldName; + } else { + // Dot in fieldName - check if there's a field whose name equals the fieldName with the last dot suffix removed + const regex = /\.[^.]*$/; + const suffixRemovedField = fieldName.replace(regex, ''); + const fieldMatch = newJobCapsService.getFieldById(suffixRemovedField); + + // There's a match - set as the main field + if (fieldMatch !== null) { + mainField = suffixRemovedField; + } else { + // No main field to be found - add the fieldName to updatedExcluded array if it's not already there + if (updatedExcluded.includes(fieldName) === false) { + updatedExcluded.push(fieldName); + } + } + } + + if (mainField !== undefined) { + // Add the main field to the updatedExcluded array if it's not already there + if (updatedExcluded.includes(mainField) === false) { + updatedExcluded.push(mainField); + } + // Create regex to find all other fields whose names begin with main field followed by a dot + const regex = new RegExp(`${mainField}\\..+`); + + // Loop through fields and add fields matching the pattern to updatedExcluded array + for (let j = 0; j < fields.length; j++) { + const field = fields[j].name; + if (updatedExcluded.includes(field) === false && field.match(regex) !== null) { + updatedExcluded.push(field); + } + } + } + } + + return updatedExcluded; +}; + export const getJobConfigFromFormState = ( formState: State['form'] ): DeepPartial => { @@ -175,7 +225,7 @@ export const getJobConfigFromFormState = ( index: formState.destinationIndex, }, analyzed_fields: { - excludes: formState.excludes, + excludes: getExcludesFields(formState.excludes), }, analysis: { outlier_detection: {}, diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 7d3ef116e67ab..c7add12be142c 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -79,19 +79,36 @@ export class MlServerPlugin implements Plugin { diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js index f7625d9eec090..a5905227f49b8 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js @@ -4,29 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); +import { act } from 'react-dom/test-utils'; + import { RemoteClusterForm } from '../../public/application/sections/components/remote_cluster_form'; -// import { pageHelpers, setupEnvironment, nextTick } from './helpers'; -import { pageHelpers, nextTick } from './helpers'; +import { pageHelpers, setupEnvironment } from './helpers'; import { REMOTE_CLUSTER_EDIT, REMOTE_CLUSTER_EDIT_NAME } from './helpers/constants'; -// const { setup } = pageHelpers.remoteClustersEdit; +const { setup } = pageHelpers.remoteClustersEdit; const { setup: setupRemoteClustersAdd } = pageHelpers.remoteClustersAdd; -// FLAKY: https://github.com/elastic/kibana/issues/57762 -// FLAKY: https://github.com/elastic/kibana/issues/57997 -// FLAKY: https://github.com/elastic/kibana/issues/57998 -describe.skip('Edit Remote cluster', () => { - // let server; - // let httpRequestsMockHelpers; +describe('Edit Remote cluster', () => { + let server; + let httpRequestsMockHelpers; let component; let find; let exists; - - /** - * - * commented out due to hooks being called regardless of skip - * https://github.com/facebook/jest/issues/8379 + let waitFor; beforeAll(() => { ({ server, httpRequestsMockHelpers } = setupEnvironment()); @@ -39,13 +32,12 @@ describe.skip('Edit Remote cluster', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); - ({ component, find, exists } = setup()); - await nextTick(100); // We need to wait next tick for the mock server response to kick in - component.update(); + await act(async () => { + ({ component, find, exists, waitFor } = setup()); + await waitFor('remoteClusterForm'); + }); }); - */ - test('should have the title of the page set correctly', () => { expect(exists('remoteClusterPageTitle')).toBe(true); expect(find('remoteClusterPageTitle').text()).toEqual('Edit remote cluster'); @@ -60,14 +52,16 @@ describe.skip('Edit Remote cluster', () => { * the "create" remote cluster, we won't test it again but simply make sure that * the form component is indeed shared between the 2 app sections. */ - test('should use the same Form component as the "" component', async () => { - const { component: addRemoteClusterComponent } = setupRemoteClustersAdd(); + test('should use the same Form component as the "" component', async () => { + let addRemoteClusterTestBed; - await nextTick(); - addRemoteClusterComponent.update(); + await act(async () => { + addRemoteClusterTestBed = setupRemoteClustersAdd(); + addRemoteClusterTestBed.waitFor('remoteClusterAddPage'); + }); const formEdit = component.find(RemoteClusterForm); - const formAdd = addRemoteClusterComponent.find(RemoteClusterForm); + const formAdd = addRemoteClusterTestBed.component.find(RemoteClusterForm); expect(formEdit.length).toBe(1); expect(formAdd.length).toBe(1); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js index 954deb8b98d3e..bc73387831c9d 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { act } from 'react-dom/test-utils'; import { pageHelpers, @@ -17,8 +18,6 @@ import { getRemoteClusterMock } from '../../fixtures/remote_cluster'; import { PROXY_MODE } from '../../common/constants'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.remoteClustersList; describe('', () => { @@ -78,6 +77,7 @@ describe('', () => { let actions; let tableCellsValues; let rows; + let waitFor; // For deterministic tests, we need to make sure that remoteCluster1 comes before remoteCluster2 // in the table list that is rendered. As the table orders alphabetically by index name @@ -110,11 +110,11 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); - // Mount the component - ({ component, find, exists, table, actions } = setup()); + await act(async () => { + ({ component, find, exists, table, actions, waitFor } = setup()); - await nextTick(100); // Make sure that the Http request is fulfilled - component.update(); + await waitFor('remoteClusterListTable'); + }); // Read the remote clusters list table ({ rows, tableCellsValues } = table.getMetaData('remoteClusterListTable')); @@ -241,8 +241,10 @@ describe('', () => { actions.clickBulkDeleteButton(); actions.clickConfirmModalDeleteRemoteCluster(); - await nextTick(600); // there is a 500ms timeout in the api action - component.update(); + await act(async () => { + await nextTick(600); // there is a 500ms timeout in the api action + component.update(); + }); ({ rows } = table.getMetaData('remoteClusterListTable')); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index 1e6c2c4d289aa..35c566548f158 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -118,9 +118,12 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u } save={[Function]} > - +
{this.renderSaveErrorFeedback()} - + diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js index 0531310bd097b..4f9c5dcd38254 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js @@ -57,7 +57,11 @@ export class RemoteClusterAdd extends PureComponent { const { isAddingCluster, addClusterError } = this.props; return ( - + { - validateFeaturePrivileges(featuresService.getFeatures()); + const features = featuresService.getFeatures(); + validateFeaturePrivileges(features); + validateReservedPrivileges(features); await registerPrivilegesWithCluster(logger, privileges, applicationName, clusterClient); }, diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 3d25fc03f568b..b023c12d35b79 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -409,13 +409,18 @@ describe('features', () => { }, privileges: null, reserved: { - privilege: { - savedObject: { - all: ['ignore-me-1', 'ignore-me-2'], - read: ['ignore-me-1', 'ignore-me-2'], + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['ignore-me-1', 'ignore-me-2'], + read: ['ignore-me-1', 'ignore-me-2'], + }, + ui: ['ignore-me-1'], + }, }, - ui: ['ignore-me-1'], - }, + ], description: '', }, }), @@ -591,13 +596,18 @@ describe('reserved', () => { }, privileges: null, reserved: { - privilege: { - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'foo', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, }, - ui: [], - }, + ], description: '', }, }), @@ -627,13 +637,18 @@ describe('reserved', () => { app: [], privileges: null, reserved: { - privilege: { - savedObject: { - all: ['savedObject-all-1', 'savedObject-all-2'], - read: ['savedObject-read-1', 'savedObject-read-2'], + privileges: [ + { + id: 'foo', + privilege: { + savedObject: { + all: ['savedObject-all-1', 'savedObject-all-2'], + read: ['savedObject-read-1', 'savedObject-read-2'], + }, + ui: ['ui-1', 'ui-2'], + }, }, - ui: ['ui-1', 'ui-2'], - }, + ], description: '', }, }), diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index b25aad30a3423..9a8935f80a174 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -110,10 +110,12 @@ export function privilegesFactory( }, reserved: features.reduce((acc: Record, feature: Feature) => { if (feature.reserved) { - acc[feature.id] = [ - actions.version, - ...featurePrivilegeBuilder.getActions(feature.reserved!.privilege, feature), - ]; + feature.reserved.privileges.forEach(reservedPrivilege => { + acc[reservedPrivilege.id] = [ + actions.version, + ...uniq(featurePrivilegeBuilder.getActions(reservedPrivilege.privilege, feature)), + ]; + }); } return acc; }, {}), diff --git a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts index ac386d287cff1..cd2c7faa263c9 100644 --- a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts @@ -26,13 +26,18 @@ it('allows features with reserved privileges to be defined', () => { privileges: null, reserved: { description: 'foo', - privilege: { - savedObject: { - all: ['foo'], - read: ['bar'], + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, }, - ui: [], - }, + ], }, }); diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts new file mode 100644 index 0000000000000..26af0dadfb288 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Feature } from '../../../features/server'; +import { validateReservedPrivileges } from './validate_reserved_privileges'; + +it('allows features to be defined without privileges', () => { + const feature: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + }); + + validateReservedPrivileges([feature]); +}); + +it('allows features with a single reserved privilege to be defined', () => { + const feature: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + validateReservedPrivileges([feature]); +}); + +it('allows multiple features with reserved privileges to be defined', () => { + const feature1: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved-1', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + const feature2: Feature = new Feature({ + id: 'foo2', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved-2', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + validateReservedPrivileges([feature1, feature2]); +}); + +it('prevents a feature from specifying the same reserved privilege id', () => { + const feature1: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + expect(() => validateReservedPrivileges([feature1])).toThrowErrorMatchingInlineSnapshot( + `"Duplicate reserved privilege id detected: reserved. This is not allowed."` + ); +}); + +it('prevents features from sharing a reserved privilege id', () => { + const feature1: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + const feature2: Feature = new Feature({ + id: 'foo2', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + expect(() => validateReservedPrivileges([feature1, feature2])).toThrowErrorMatchingInlineSnapshot( + `"Duplicate reserved privilege id detected: reserved. This is not allowed."` + ); +}); diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts new file mode 100644 index 0000000000000..0915308fc0f89 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Feature } from '../../../features/server'; + +export function validateReservedPrivileges(features: Feature[]) { + const seenPrivilegeIds = new Set(); + + for (const feature of features) { + (feature?.reserved?.privileges ?? []).forEach(({ id }) => { + if (seenPrivilegeIds.has(id)) { + throw new Error(`Duplicate reserved privilege id detected: ${id}. This is not allowed.`); + } + seenPrivilegeIds.add(id); + }); + } +} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx index 7809b511adda4..99b4e184c071a 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx @@ -6,7 +6,6 @@ import React from 'react'; import Boom from 'boom'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../src/legacy/core_plugins/management/public/np_ready/mocks'; import { CopySavedObjectsToSpaceFlyout } from './copy_to_space_flyout'; import { CopyToSpaceForm } from './copy_to_space_form'; import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; @@ -19,11 +18,6 @@ import { spacesManagerMock } from '../../spaces_manager/mocks'; import { SpacesManager } from '../../spaces_manager'; import { ToastsApi } from 'src/core/public'; -jest.mock('../../../../../../src/legacy/core_plugins/management/public/legacy', () => ({ - setup: mockManagementPlugin.createSetupContract(), - start: mockManagementPlugin.createStartContract(), -})); - interface SetupOpts { mockSpaces?: Space[]; returnBeforeSpacesLoad?: boolean; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx index 4d92505c4aebb..fee41fc7e36d3 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx @@ -25,7 +25,7 @@ import { ToastsStart } from 'src/core/public'; import { ProcessedImportResponse, processImportResponse, -} from '../../../../../../src/legacy/core_plugins/management/public'; +} from '../../../../../../src/legacy/core_plugins/kibana/public'; import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; import { Space } from '../../../common/model/space'; import { SpacesManager } from '../../spaces_manager'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx index b22cec0af5ea8..4f6ff55dbfbb2 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiStat, EuiHorizontalRule } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/kibana/public'; import { ImportRetry } from '../types'; interface Props { diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index 96cbac4b48065..ea74fc92b95ea 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -13,7 +13,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/kibana/public'; import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; import { Space } from '../../../common/model/space'; import { CopyOptions, ImportRetry } from '../types'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts index fb2616619c644..65a0cabfeb716 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts @@ -5,7 +5,7 @@ */ import { summarizeCopyResult } from './summarize_copy_result'; -import { ProcessedImportResponse } from 'src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from 'src/legacy/core_plugins/kibana/public'; const createSavedObjectsManagementRecord = () => ({ type: 'dashboard', diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts index 96e642b0f45d8..44c9e9993bf10 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessedImportResponse } from 'src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from 'src/legacy/core_plugins/kibana/public'; import { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; export interface SummarizedSavedObjectResult { diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index f5f9e98fe659c..feff17b813112 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -29,6 +29,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const savedSearches = createSavedSearchesLoader({ savedObjectsClient, indexPatterns, + search: appDeps.data.search, chrome: appDeps.chrome, overlays: appDeps.overlays, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d07026c0883b8..705a4577cbd07 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -130,7 +130,6 @@ "charts.colormaps.greysText": "グレー", "charts.colormaps.redsText": "赤", "charts.colormaps.yellowToRedText": "黄色から赤", - "common.ui.aggResponse.allDocsTitle": "すべてのドキュメント", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "エラー", "common.ui.errorAutoCreateIndex.errorDescription": "Elasticsearch クラスターの {autoCreateIndexActionConfig} 設定が原因で、Kibana が保存されたオブジェクトを格納するインデックスを自動的に作成できないようです。Kibana は、保存されたオブジェクトインデックスが適切なマッピング/スキーマを使用し Kibana から Elasticsearch へのポーリングの回数を減らすための最適な手段であるため、この Elasticsearch の機能を使用します。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "申し訳ございませんが、この問題が解決されるまで Kibana で何も保存することができません。", @@ -1196,16 +1195,12 @@ "home.tutorials.common.auditbeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.auditbeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.auditbeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.auditbeatCloudInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.auditbeatCloudInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.auditbeatInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。", @@ -1247,16 +1242,12 @@ "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.filebeatCloudInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.filebeatCloudInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatCloudInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.filebeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.filebeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatCloudInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.filebeatEnableInstructions.debTextPost": "「/etc/filebeat/modules.d/{moduleName}.yml」ファイルで設定を変更します。", @@ -1311,10 +1302,8 @@ "home.tutorials.common.functionbeatAWSInstructions.textPost": "「」と「」がアカウント資格情報、「us-east-1」がご希望の地域です。", "home.tutorials.common.functionbeatAWSInstructions.textPre": "環境で AWS アカウント認証情報を設定します。", "home.tutorials.common.functionbeatAWSInstructions.title": "AWS 認証情報の設定", - "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.functionbeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.functionbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTextPost": "「」が投入するロググループの名前で、「」が Functionbeat デプロイのステージングに使用されるが有効な S3 バケット名です。", @@ -1345,16 +1334,12 @@ "home.tutorials.common.heartbeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.heartbeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.heartbeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.heartbeatCloudInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.heartbeatCloudInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatCloudInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatCloudInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.heartbeatEnableCloudInstructions.debTextPre": "「heartbeat.yml」ファイルの「heartbeat.monitors」設定を変更します。", @@ -1414,16 +1399,12 @@ "home.tutorials.common.metricbeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.metricbeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.metricbeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.metricbeatCloudInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.metricbeatCloudInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatCloudInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatCloudInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.metricbeatEnableInstructions.debTextPost": "「/etc/metricbeat/modules.d/{moduleName}.yml」ファイルで設定を変更します。", @@ -1478,7 +1459,6 @@ "home.tutorials.common.winlogbeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.winlogbeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.winlogbeatInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。", @@ -2523,12 +2503,10 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "データに接続", "management.displayName": "管理", - "management.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", - "management.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", - "management.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", - "management.editIndexPattern.list.defaultIndexPatternListName": "デフォルト", - "management.indexPatternHeader": "インデックスパターン", - "management.indexPatternLabel": "Elasticsearch からのデータの取得に役立つインデックスパターンを管理します。", + "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", + "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", + "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", + "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "デフォルト", "management.nav.label": "管理", "management.nav.menu": "管理メニュー", "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", @@ -3830,6 +3808,7 @@ "visTypeVega.visualization.renderErrorTitle": "Vega エラー", "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "デフォルトのインデックスが見つかりません", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "データなしにはレンダリングできません", + "visTypeVislib.aggResponse.allDocsTitle": "すべてのドキュメント", "visTypeVislib.area.areaDescription": "折れ線グラフの下の数量を強調します。", "visTypeVislib.area.areaTitle": "エリア", "visTypeVislib.area.countText": "カウント", @@ -15866,10 +15845,7 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "AND", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "タイミング", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "タイミング", - "xpack.triggersActionsUI.components.alertActionSecurityCallOut.enableTlsCta": "TLS を有効にする", - "xpack.triggersActionsUI.components.alertActionSecurityCallOut.tlsDisabledTitle": "アラート {action} を実行するには Elasticsearch と Kibana の間に TLS が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "メールに送信", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton": "変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "サーバーからメールを送信します。", "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "送信元は有効なメールアドレスではありません。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "To、Cc、または Bcc のエントリーがありません。 1 つ以上のエントリーが必要です。", @@ -15898,14 +15874,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip": "このチェックボックスは更新インデックス値を設定します。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText": "データを Elasticsearch にインデックスしてください。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "PagerDuty に送信", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton1": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton2": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton3": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton4": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton5": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton6": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton7": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariableTitle": "アラート変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "クラス (任意)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.componentTextFieldLabel": "コンポーネント(任意)", @@ -15929,14 +15897,10 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "まとめ", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "タイムスタンプ (任意)", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle": "サーバーログに送信", - "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariablePopoverButton": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariableTitle": "変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logLevelFieldLabel": "レベル", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "メッセージ", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "Kibana ログにメッセージを追加します。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "Slack に送信", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariablePopoverButton": "アラート変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariableTitle": "アラート変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "Web フック URL が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "メッセージ", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "Slack チャネルにメッセージを送信します。", @@ -15945,8 +15909,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Web フックデータ", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "ヘッダーを追加", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "追加", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariablePopoverButton": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariableTitle": "変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "コードエディター", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "本文", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "削除", @@ -15962,9 +15924,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "HTTP ヘッダーを追加", "xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText": "{numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}を削除できませんでした", "xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText": "{numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}を削除しました", - "xpack.triggersActionsUI.components.securityCallOut.enableTlsCta": "TLS を有効にする", - "xpack.triggersActionsUI.components.securityCallOut.tlsDisabledDescription": "アラートは API キー に依存し、キーを使用するには Elasticsearch と Kibana の間に TLS が必要です。", - "xpack.triggersActionsUI.components.securityCallOut.tlsDisabledTitle": "トランスポートレイヤーセキュリティを有効にする", "xpack.triggersActionsUI.connectors.breadcrumbTitle": "コネクター", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "{numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}}を削除 ", @@ -15988,8 +15947,8 @@ "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名前が必要です。", "xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle": "さらにアクションを表示", "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel": "コネクターを作成", - "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyBody": "Kibana でトリガーできるメール、Slack, Elasticsearch、およびサードパーティサービスを構成します。", - "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyTitle": "初めてのコネクターを作成する", + "xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyBody": "Kibana でトリガーできるメール、Slack, Elasticsearch、およびサードパーティサービスを構成します。", + "xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyTitle": "初めてのコネクターを作成する", "xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteDisabledTitle": "コネクターを削除できません", "xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteLabel": "{count} 件を削除", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription": "このコネクターを削除", @@ -16039,7 +15998,6 @@ "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました", - "xpack.triggersActionsUI.sections.alertAdd.securityCalloutAction": "作成", "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください。", "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "閉じる", "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", @@ -16076,7 +16034,6 @@ "xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "アラートを更新できません。", "xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "「{alertName}」 を更新しました", - "xpack.triggersActionsUI.sections.alertEdit.securityCalloutAction": "編集中", "xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel": "削除", "xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle": "このアクションは無効です", "xpack.triggersActionsUI.sections.alertForm.actionIdLabel": "{connectorInstance} コネクター", @@ -16128,9 +16085,9 @@ "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "有効にする", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "アクション", - "xpack.triggersActionsUI.sections.alertsList.emptyButton": "アラートの作成", - "xpack.triggersActionsUI.sections.alertsList.emptyDesc": "トリガーが起きたときにメール、Slack、または別のコネクターを通してアラートを受信します。", - "xpack.triggersActionsUI.sections.alertsList.emptyTitle": "初めてのアラートを作成する", + "xpack.triggersActionsUI.components.emptyPrompt.emptyButton": "アラートの作成", + "xpack.triggersActionsUI.components.emptyPrompt.emptyDesc": "トリガーが起きたときにメール、Slack、または別のコネクターを通してアラートを受信します。", + "xpack.triggersActionsUI.components.emptyPrompt.emptyTitle": "初めてのアラートを作成する", "xpack.triggersActionsUI.sections.alertsList.multipleTitle": "アラート", "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "検索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "アラート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7e64ff6301faf..50b807a4934ed 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -130,7 +130,6 @@ "charts.colormaps.greysText": "灰色", "charts.colormaps.redsText": "红色", "charts.colormaps.yellowToRedText": "黄到红", - "common.ui.aggResponse.allDocsTitle": "所有文档", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "错误", "common.ui.errorAutoCreateIndex.errorDescription": "似乎 Elasticsearch 集群的 {autoCreateIndexActionConfig} 设置使 Kibana 无法自动创建用于存储已保存对象的索引。Kibana 将使用此 Elasticsearch 功能,因为这是确保已保存对象索引使用正确映射/架构的最好方式,而且其允许 Kibana 较少地轮询 Elasticsearch。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "但是,只有解决了此问题后,您才能在 Kibana 保存内容。", @@ -1197,16 +1196,12 @@ "home.tutorials.common.auditbeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.auditbeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.auditbeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.auditbeatCloudInstructions.config.debTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.auditbeatCloudInstructions.config.debTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.auditbeatInstructions.config.debTextPost": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。", @@ -1248,16 +1243,12 @@ "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.filebeatCloudInstructions.config.debTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.filebeatCloudInstructions.config.debTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.filebeatCloudInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.filebeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.filebeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.filebeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.filebeatCloudInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.filebeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.filebeatEnableInstructions.debTextPost": "在 `/etc/filebeat/modules.d/{moduleName}.yml` 文件中修改设置。", @@ -1312,10 +1303,8 @@ "home.tutorials.common.functionbeatAWSInstructions.textPost": "其中 `` 和 `` 是您的帐户凭据,`us-east-1` 是所需的地区。", "home.tutorials.common.functionbeatAWSInstructions.textPre": "在环境中设置您的 AWS 帐户凭据:", "home.tutorials.common.functionbeatAWSInstructions.title": "设置 AWS 凭据", - "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.functionbeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.functionbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTextPost": "其中 `` 是要采集的日志组名称,`` 是将用于暂存 Functionbeat 部署的有效 S3 存储桶名称。", @@ -1346,16 +1335,12 @@ "home.tutorials.common.heartbeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.heartbeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.heartbeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.heartbeatCloudInstructions.config.debTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.heartbeatCloudInstructions.config.debTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.heartbeatCloudInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.heartbeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.heartbeatCloudInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.heartbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.heartbeatEnableCloudInstructions.debTextPre": "在 `heartbeat.yml` 文件中编辑 `heartbeat.monitors` 设置。", @@ -1415,16 +1400,12 @@ "home.tutorials.common.metricbeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.metricbeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.metricbeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.metricbeatCloudInstructions.config.debTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.metricbeatCloudInstructions.config.debTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.metricbeatCloudInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.metricbeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.metricbeatCloudInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.metricbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.metricbeatEnableInstructions.debTextPost": "在 `/etc/metricbeat/modules.d/{moduleName}.yml` 文件中修改设置。", @@ -1479,7 +1460,6 @@ "home.tutorials.common.winlogbeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.winlogbeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.winlogbeatInstructions.config.windowsTextPost": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。", @@ -2524,12 +2504,10 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "连接数据", "management.displayName": "管理", - "management.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", - "management.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", - "management.editIndexPattern.createIndex.defaultTypeName": "索引模式", - "management.editIndexPattern.list.defaultIndexPatternListName": "默认值", - "management.indexPatternHeader": "索引模式", - "management.indexPatternLabel": "管理帮助从 Elasticsearch 检索数据的索引模式。", + "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", + "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", + "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "索引模式", + "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "默认值", "management.nav.label": "管理", "management.nav.menu": "管理菜单", "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", @@ -3831,6 +3809,7 @@ "visTypeVega.visualization.renderErrorTitle": "Vega 错误", "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "找不到默认索引", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "没有数据时无法渲染", + "visTypeVislib.aggResponse.allDocsTitle": "所有文档", "visTypeVislib.area.areaDescription": "突出折线图下方的数量", "visTypeVislib.area.areaTitle": "面积图", "visTypeVislib.area.countText": "计数", @@ -15870,10 +15849,7 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "且", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "当", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "当", - "xpack.triggersActionsUI.components.alertActionSecurityCallOut.enableTlsCta": "启用 TLS", - "xpack.triggersActionsUI.components.alertActionSecurityCallOut.tlsDisabledTitle": "告警 {action} 在 Elasticsearch 和 Kibana 之间需要 TLS。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "发送到电子邮件", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton": "添加变量", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "从您的服务器发送电子邮件。", "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "发送者电子邮件地址无效。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "未输入收件人、抄送、密送。 至少需要输入一个。", @@ -15902,14 +15878,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip": "此复选框设置刷新索引值。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText": "将数据索引到 Elasticsearch 中。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "发送到 PagerDuty", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton1": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton2": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton3": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton4": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton5": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton6": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton7": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariableTitle": "添加告警变量", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "类(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.componentTextFieldLabel": "组件(可选)", @@ -15933,14 +15901,10 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "摘要", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "时间戳(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle": "发送到服务器日志", - "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariablePopoverButton": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariableTitle": "添加变量", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logLevelFieldLabel": "级别", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "消息", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "将消息添加到 Kibana 日志。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "发送到 Slack", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariablePopoverButton": "添加告警变量", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariableTitle": "添加告警变量", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "Webhook URL 必填。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "消息", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "向 Slack 频道或用户发送消息。", @@ -15949,8 +15913,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Webhook 数据", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "添加标头", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "添加", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariablePopoverButton": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariableTitle": "添加变量", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "代码编辑器", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "正文", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "删除", @@ -15966,9 +15928,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "添加 HTTP 标头", "xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText": "无法删除 {numErrors, number} 个{numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}", "xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText": "已删除 {numSuccesses, number} 个{numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}", - "xpack.triggersActionsUI.components.securityCallOut.enableTlsCta": "启用 TLS", - "xpack.triggersActionsUI.components.securityCallOut.tlsDisabledDescription": "Alerting 依赖于在 Elasticsearch 和 Kibana 之间需要 TLS 的 API 密钥。", - "xpack.triggersActionsUI.components.securityCallOut.tlsDisabledTitle": "启用传输层安全", "xpack.triggersActionsUI.connectors.breadcrumbTitle": "连接器", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other { # 个{multipleTitle}}} ", @@ -15993,8 +15952,8 @@ "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名称必填。", "xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle": "获取更多的操作", "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel": "创建连接器", - "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyBody": "配置电子邮件、Slack、Elasticsearch 和 Kibana 可以触发的第三方服务。", - "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyTitle": "创建您的首个连接器", + "xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyBody": "配置电子邮件、Slack、Elasticsearch 和 Kibana 可以触发的第三方服务。", + "xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyTitle": "创建您的首个连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteDisabledTitle": "无法删除连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteLabel": "删除 {count} 个", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription": "删除此连接器", @@ -16044,7 +16003,6 @@ "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”", - "xpack.triggersActionsUI.sections.alertAdd.securityCalloutAction": "创建", "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引。", "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "关闭", "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", @@ -16081,7 +16039,6 @@ "xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "无法更新告警。", "xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "已更新“{alertName}”", - "xpack.triggersActionsUI.sections.alertEdit.securityCalloutAction": "正在编辑", "xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel": "删除", "xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle": "此操作已禁用", "xpack.triggersActionsUI.sections.alertForm.actionIdLabel": "{connectorInstance} 连接器", @@ -16133,9 +16090,9 @@ "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "启用", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "操作", - "xpack.triggersActionsUI.sections.alertsList.emptyButton": "创建告警", - "xpack.triggersActionsUI.sections.alertsList.emptyDesc": "触发条件满足时通过电子邮件、Slack 或其他连接器接收告警。", - "xpack.triggersActionsUI.sections.alertsList.emptyTitle": "创建您的首个告警", + "xpack.triggersActionsUI.components.emptyPrompt.emptyButton": "创建告警", + "xpack.triggersActionsUI.components.emptyPrompt.emptyDesc": "触发条件满足时通过电子邮件、Slack 或其他连接器接收告警。", + "xpack.triggersActionsUI.components.emptyPrompt.emptyTitle": "创建您的首个告警", "xpack.triggersActionsUI.sections.alertsList.multipleTitle": "告警", "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "搜索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "告警", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss new file mode 100644 index 0000000000000..996f21c4b6b09 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss @@ -0,0 +1,4 @@ +.messageVariablesPanel { + @include euiYScrollWithShadows; + max-height: $euiSize * 20; +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx new file mode 100644 index 0000000000000..ab9b5c2586c17 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import './add_message_variables.scss'; + +interface Props { + messageVariables: string[] | undefined; + paramsProperty: string; + onSelectEventHandler: (variable: string) => void; +} + +export const AddMessageVariables: React.FunctionComponent = ({ + messageVariables, + paramsProperty, + onSelectEventHandler, +}) => { + const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); + + const getMessageVariables = () => + messageVariables?.map((variable: string) => ( + { + onSelectEventHandler(variable); + setIsVariablesPopoverOpen(false); + }} + > + {`{{${variable}}}`} + + )); + + const addVariableButtonTitle = i18n.translate( + 'xpack.triggersActionsUI.components.addMessageVariables.addVariableTitle', + { + defaultMessage: 'Add alert variable', + } + ); + + return ( + setIsVariablesPopoverOpen(true)} + iconType="indexOpen" + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton', + { + defaultMessage: 'Add variable', + } + )} + /> + } + isOpen={isVariablesPopoverOpen} + closePopover={() => setIsVariablesPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.test.tsx deleted file mode 100644 index 85699cfbd750f..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.test.tsx +++ /dev/null @@ -1,78 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { Fragment } from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { AlertActionSecurityCallOut } from './alert_action_security_call_out'; - -import { EuiCallOut, EuiButton } from '@elastic/eui'; -import { act } from 'react-dom/test-utils'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; - -const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; - -const http = httpServiceMock.createStartContract(); - -describe('alert action security call out', () => { - let useEffect: any; - - const mockUseEffect = () => { - // make react execute useEffects despite shallow rendering - useEffect.mockImplementationOnce((f: Function) => f()); - }; - - beforeEach(() => { - jest.resetAllMocks(); - useEffect = jest.spyOn(React, 'useEffect'); - mockUseEffect(); - }); - - test('renders nothing while health is loading', async () => { - http.get.mockImplementationOnce(() => new Promise(() => {})); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow( - - ); - }); - - expect(component?.is(Fragment)).toBeTruthy(); - expect(component?.html()).toBe(''); - }); - - test('renders nothing if keys are enabled', async () => { - http.get.mockResolvedValue({ isSufficientlySecure: true }); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow( - - ); - }); - - expect(component?.is(Fragment)).toBeTruthy(); - expect(component?.html()).toBe(''); - }); - - test('renders the callout if keys are disabled', async () => { - http.get.mockResolvedValue({ isSufficientlySecure: false }); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow( - - ); - }); - - expect(component?.find(EuiCallOut).prop('title')).toMatchInlineSnapshot( - `"Alert creation requires TLS between Elasticsearch and Kibana."` - ); - - expect(component?.find(EuiButton).prop('href')).toMatchInlineSnapshot( - `"elastic.co/guide/en/kibana/current/configuring-tls.html"` - ); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.tsx deleted file mode 100644 index f7a80202dff89..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.tsx +++ /dev/null @@ -1,78 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { Option, none, some, fold, filter } from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { DocLinksStart, HttpSetup } from 'kibana/public'; -import { AlertingFrameworkHealth } from '../../types'; -import { health } from '../lib/alert_api'; - -interface Props { - docLinks: Pick; - action: string; - http: HttpSetup; -} - -export const AlertActionSecurityCallOut: React.FunctionComponent = ({ - http, - action, - docLinks, -}) => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; - - const [alertingHealth, setAlertingHealth] = React.useState>(none); - - React.useEffect(() => { - async function fetchSecurityConfigured() { - setAlertingHealth(some(await health({ http }))); - } - - fetchSecurityConfigured(); - }, [http]); - - return pipe( - alertingHealth, - filter(healthCheck => !healthCheck.isSufficientlySecure), - fold( - () => , - () => ( - - - - - - - - - ) - ) - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx index a7d479f922ed1..af9e34071fd09 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx @@ -39,6 +39,7 @@ describe('connector validation', () => { id: 'test', actionTypeId: '.email', name: 'email', + isPreconfigured: false, config: { from: 'test@test.com', port: 2323, @@ -66,6 +67,7 @@ describe('connector validation', () => { }, id: 'test', actionTypeId: '.email', + isPreconfigured: false, name: 'email', config: { from: 'test@test.com', @@ -117,6 +119,7 @@ describe('connector validation', () => { }, id: 'test', actionTypeId: '.email', + isPreconfigured: false, name: 'email', config: { from: 'test@test.com', @@ -144,6 +147,7 @@ describe('connector validation', () => { }, id: 'test', actionTypeId: '.email', + isPreconfigured: false, name: 'email', config: { from: 'test@test.com', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx index b4bbb8af36a19..dff697297f3e4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx @@ -16,10 +16,6 @@ import { EuiButtonEmpty, EuiSwitch, EuiFormRow, - EuiContextMenuItem, - EuiButtonIcon, - EuiContextMenuPanel, - EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { @@ -29,6 +25,7 @@ import { ActionParamsProps, } from '../../../types'; import { EmailActionParams, EmailActionConnector } from './types'; +import { AddMessageVariables } from '../add_message_variables'; export function getActionType(): ActionTypeModel { const mailformat = /^[^@\s]+@[^@\s]+$/; @@ -368,25 +365,21 @@ const EmailParamsFields: React.FunctionComponent(false); const [addBCC, setAddBCC] = useState(false); - const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); useEffect(() => { if (!message && defaultMessage && defaultMessage.length > 0) { editAction('message', defaultMessage, index); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const messageVariablesItems = messageVariables?.map((variable: string) => ( - { - editAction('message', (message ?? '').concat(` {{${variable}}}`), index); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction( + paramsProperty, + ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), + index + ); + }; + return ( + onSelectMessageVariable('subject', variable) + } + paramsProperty="subject" + /> + } > setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton', - { - defaultMessage: 'Add variable', - } - )} - /> + + onSelectMessageVariable('message', variable) } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + paramsProperty="message" + /> } > { ).toBe(`{ "test": 123 }`); - expect( - wrapper.find('[data-test-subj="indexDocumentAddVariableButton"]').length > 0 - ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 56d9f40e40021..9bd6a39d216e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -14,10 +14,6 @@ import { EuiSelect, EuiTitle, EuiIconTip, - EuiPopover, - EuiButtonIcon, - EuiContextMenuPanel, - EuiContextMenuItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -36,6 +32,7 @@ import { getIndexPatterns, } from '../../../common/index_controls'; import { useXJsonMode } from '../../lib/use_x_json_mode'; +import { AddMessageVariables } from '../add_message_variables'; export function getActionType(): ActionTypeModel { return { @@ -282,23 +279,13 @@ const IndexParamsFields: React.FunctionComponent 0 ? documents[0] : null ); - const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); - const messageVariablesItems = messageVariables?.map((variable: string, i: number) => ( - { - const value = (xJson ?? '').concat(` {{${variable}}}`); - setXJson(value); - // Keep the documents in sync with the editor content - onDocumentsChange(convertToJson(value)); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + const onSelectMessageVariable = (variable: string) => { + const value = (xJson ?? '').concat(` {{${variable}}}`); + setXJson(value); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(value)); + }; + function onDocumentsChange(updatedDocuments: string) { try { const documentsJSON = JSON.parse(updatedDocuments); @@ -317,34 +304,11 @@ const IndexParamsFields: React.FunctionComponent setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - title={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.addVariableTitle', - { - defaultMessage: 'Add variable', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.addVariablePopoverButton', - { - defaultMessage: 'Add variable', - } - )} - /> - } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + onSelectMessageVariable(variable)} + paramsProperty="documents" + /> } > >({ - dedupKey: false, - summary: false, - source: false, - timestamp: false, - component: false, - group: false, - class: false, - }); - // TODO: replace this button with a proper Eui component, when it will be ready - const getMessageVariables = (paramsProperty: string) => - messageVariables?.map((variable: string) => ( - { - editAction( - paramsProperty, - ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), - index - ); - setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: false }); - }} - > - {`{{${variable}}}`} - - )); - - const addVariableButtonTitle = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariableTitle', - { - defaultMessage: 'Add alert variable', - } - ); - const getAddVariableComponent = (paramsProperty: string, buttonName: string) => { - return ( - - setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: true }) - } - iconType="indexOpen" - aria-label={buttonName} - /> - } - isOpen={isVariablesPopoverOpen[paramsProperty]} - closePopover={() => - setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: false }) - } - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction( + paramsProperty, + ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), + index ); }; + return ( @@ -359,15 +305,15 @@ const PagerDutyParamsFields: React.FunctionComponent + onSelectMessageVariable('dedupKey', variable) } - ) - )} + paramsProperty="dedupKey" + /> + } > + onSelectMessageVariable('timestamp', variable) } - ) - )} + paramsProperty="timestamp" + /> + } > + onSelectMessageVariable('component', variable) } - ) - )} + paramsProperty="component" + /> + } > onSelectMessageVariable('group', variable)} + paramsProperty="group" + /> + } > onSelectMessageVariable('source', variable)} + paramsProperty="source" + /> + } > + onSelectMessageVariable('summary', variable) } - ) - )} + paramsProperty="summary" + /> + } > onSelectMessageVariable('class', variable)} + paramsProperty="class" + /> + } > (false); useEffect(() => { editAction('level', 'info', index); @@ -80,18 +72,11 @@ export const ServerLogParamsFields: React.FunctionComponent ( - { - editAction('message', (message ?? '').concat(` {{${variable}}}`), index); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); + }; + return ( setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - title={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariableTitle', - { - defaultMessage: 'Add variable', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariablePopoverButton', - { - defaultMessage: 'Add variable', - } - )} - /> + + onSelectMessageVariable('message', variable) } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + paramsProperty="message" + /> } > { const { message } = actionParams; - const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); useEffect(() => { if (!message && defaultMessage && defaultMessage.length > 0) { editAction('message', defaultMessage, index); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const messageVariablesItems = messageVariables?.map((variable: string, i: number) => ( - { - editAction('message', (message ?? '').concat(` {{${variable}}}`), index); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); + }; + return ( setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - title={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariableTitle', - { - defaultMessage: 'Add alert variable', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariablePopoverButton', - { - defaultMessage: 'Add alert variable', - } - )} - /> + + onSelectMessageVariable('message', variable) } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + paramsProperty="message" + /> } > { id: 'test', actionTypeId: '.webhook', name: 'webhook', + isPreconfigured: false, config: { method: 'PUT', url: 'http:\\test', @@ -106,6 +107,7 @@ describe('WebhookActionConnectorFields renders', () => { }, id: 'test', actionTypeId: '.webhook', + isPreconfigured: false, name: 'webhook', config: { method: 'PUT', @@ -160,7 +162,7 @@ describe('WebhookParamsFields renders', () => { .first() .prop('value') ).toStrictEqual('test message'); - expect(wrapper.find('[data-test-subj="webhookAddVariableButton"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); }); test('params validation fails when body is not valid', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx index f611c3715e56a..daa5a6caeabe9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx @@ -22,9 +22,6 @@ import { EuiCodeEditor, EuiSwitch, EuiButtonEmpty, - EuiContextMenuItem, - EuiPopover, - EuiContextMenuPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { @@ -34,6 +31,7 @@ import { ActionParamsProps, } from '../../../types'; import { WebhookActionParams, WebhookActionConnector } from './types'; +import { AddMessageVariables } from '../add_message_variables'; const HTTP_VERBS = ['post', 'put']; @@ -467,20 +465,9 @@ const WebhookParamsFields: React.FunctionComponent { const { body } = actionParams; - const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); - const messageVariablesItems = messageVariables?.map((variable: string, i: number) => ( - { - editAction('body', (body ?? '').concat(` {{${variable}}}`), index); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index); + }; return ( setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - title={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariableTitle', - { - defaultMessage: 'Add variable', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariablePopoverButton', - { - defaultMessage: 'Add variable', - } - )} - /> - } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + onSelectMessageVariable('body', variable)} + paramsProperty="body" + /> } > { + test('renders spinner while health is loading', async () => { + http.get.mockImplementationOnce(() => new Promise(() => {})); + + const { queryByText, container } = render( + +

{'shouldnt render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + expect(container.getElementsByClassName('euiLoadingSpinner').length).toBe(1); + expect(queryByText('shouldnt render')).not.toBeInTheDocument(); + }); + + it('renders children if keys are enabled', async () => { + http.get.mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true }); + + const { queryByText } = render( + +

{'should render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + expect(queryByText('should render')).toBeInTheDocument(); + }); + + test('renders warning if keys are disabled', async () => { + http.get.mockImplementationOnce(async () => ({ + isSufficientlySecure: false, + hasPermanentEncryptionKey: true, + })); + + const { queryAllByText } = render( + +

{'should render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + const [description, action] = queryAllByText(/TLS/i); + + expect(description.textContent).toMatchInlineSnapshot( + `"Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. Learn how to enable TLS."` + ); + + expect(action.textContent).toMatchInlineSnapshot(`"Learn how to enable TLS."`); + + expect(action.getAttribute('href')).toMatchInlineSnapshot( + `"elastic.co/guide/en/kibana/current/configuring-tls.html"` + ); + }); + + test('renders warning if encryption key is ephemeral', async () => { + http.get.mockImplementationOnce(async () => ({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: false, + })); + + const { queryByText, queryByRole } = render( + +

{'should render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + const description = queryByRole(/banner/i); + expect(description!.textContent).toMatchInlineSnapshot( + `"To create an alert, set a value for xpack.encrypted_saved_objects.encryptionKey in your kibana.yml file. Learn how."` + ); + + const action = queryByText(/Learn/i); + expect(action!.textContent).toMatchInlineSnapshot(`"Learn how."`); + expect(action!.getAttribute('href')).toMatchInlineSnapshot( + `"elastic.co/guide/en/kibana/current/alert-action-settings-kb.html#general-alert-action-settings"` + ); + }); + + test('renders warning if encryption key is ephemeral and keys are disabled', async () => { + http.get.mockImplementationOnce(async () => ({ + isSufficientlySecure: false, + hasPermanentEncryptionKey: false, + })); + + const { queryByText } = render( + +

{'should render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + const description = queryByText(/Transport Layer Security/i); + + expect(description!.textContent).toMatchInlineSnapshot( + `"You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. Learn how"` + ); + + const action = queryByText(/Learn/i); + expect(action!.textContent).toMatchInlineSnapshot(`"Learn how"`); + expect(action!.getAttribute('href')).toMatchInlineSnapshot( + `"elastic.co/guide/en/kibana/current/alerting-getting-started.html#alerting-setup-prerequisites"` + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx new file mode 100644 index 0000000000000..c967cf5de0771 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { Option, none, some, fold } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiLink, EuiLoadingSpinner } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DocLinksStart, HttpSetup } from 'kibana/public'; + +import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; +import { AlertingFrameworkHealth } from '../../types'; +import { health } from '../lib/alert_api'; +import './health_check.scss'; + +interface Props { + docLinks: Pick; + http: HttpSetup; + inFlyout?: boolean; +} + +export const HealthCheck: React.FunctionComponent = ({ + docLinks, + http, + children, + inFlyout = false, +}) => { + const [alertingHealth, setAlertingHealth] = React.useState>(none); + + React.useEffect(() => { + (async function() { + setAlertingHealth(some(await health({ http }))); + })(); + }, [http]); + + const className = inFlyout ? 'alertingFlyoutHealthCheck' : 'alertingHealthCheck'; + + return pipe( + alertingHealth, + fold( + () => , + healthCheck => { + return healthCheck?.isSufficientlySecure && healthCheck?.hasPermanentEncryptionKey ? ( + {children} + ) : !healthCheck.isSufficientlySecure && !healthCheck.hasPermanentEncryptionKey ? ( + + ) : !healthCheck.hasPermanentEncryptionKey ? ( + + ) : ( + + ); + } + ) + ); +}; + +type PromptErrorProps = Pick & { + className?: string; +}; + +const TlsAndEncryptionError = ({ + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + className, +}: PromptErrorProps) => ( + + + + } + body={ +
+

+ {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionError', { + defaultMessage: + 'You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. ', + })} + + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorAction', + { + defaultMessage: 'Learn how', + } + )} + +

+
+ } + /> +); + +const EncryptionError = ({ + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + className, +}: PromptErrorProps) => ( + + + + } + body={ +
+

+ {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorBeforeKey', + { + defaultMessage: 'To create an alert, set a value for ', + } + )} + {'xpack.encrypted_saved_objects.encryptionKey'} + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAfterKey', + { + defaultMessage: ' in your kibana.yml file. ', + } + )} + + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAction', + { + defaultMessage: 'Learn how.', + } + )} + +

+
+ } + /> +); + +const TlsError = ({ + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + className, +}: PromptErrorProps) => ( + + + + } + body={ +
+

+ {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsError', { + defaultMessage: + 'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ', + })} + + {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsErrorAction', { + defaultMessage: 'Learn how to enable TLS.', + })} + +

+
+ } + /> +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.scss new file mode 100644 index 0000000000000..fe001ce294ef4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.scss @@ -0,0 +1,3 @@ +.actEmptyConnectorsPrompt__logo + .actEmptyConnectorsPrompt__logo { + margin-left: $euiSize; +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx new file mode 100644 index 0000000000000..0e956ea56faa9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Fragment } from 'react'; +import { EuiButton, EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui'; +import './empty_connectors_prompt.scss'; + +export const EmptyConnectorsPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => ( + + + + + + +

+ +

+
+
+ } + body={ +

+ +

+ } + actions={ + + + + } + /> +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx new file mode 100644 index 0000000000000..df593d587de3f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; + +export const EmptyPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => ( + + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.test.tsx deleted file mode 100644 index 28bc02ec3392f..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.test.tsx +++ /dev/null @@ -1,72 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { Fragment } from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { SecurityEnabledCallOut } from './security_call_out'; - -import { EuiCallOut, EuiButton } from '@elastic/eui'; -import { act } from 'react-dom/test-utils'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; - -const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; - -const http = httpServiceMock.createStartContract(); - -describe('security call out', () => { - let useEffect: any; - - const mockUseEffect = () => { - // make react execute useEffects despite shallow rendering - useEffect.mockImplementationOnce((f: Function) => f()); - }; - - beforeEach(() => { - jest.resetAllMocks(); - useEffect = jest.spyOn(React, 'useEffect'); - mockUseEffect(); - }); - - test('renders nothing while health is loading', async () => { - http.get.mockImplementationOnce(() => new Promise(() => {})); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow(); - }); - - expect(component?.is(Fragment)).toBeTruthy(); - expect(component?.html()).toBe(''); - }); - - test('renders nothing if keys are enabled', async () => { - http.get.mockResolvedValue({ isSufficientlySecure: true }); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow(); - }); - - expect(component?.is(Fragment)).toBeTruthy(); - expect(component?.html()).toBe(''); - }); - - test('renders the callout if keys are disabled', async () => { - http.get.mockImplementationOnce(async () => ({ isSufficientlySecure: false })); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow(); - }); - - expect(component?.find(EuiCallOut).prop('title')).toMatchInlineSnapshot( - `"Enable Transport Layer Security"` - ); - - expect(component?.find(EuiButton).prop('href')).toMatchInlineSnapshot( - `"elastic.co/guide/en/kibana/current/configuring-tls.html"` - ); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.tsx deleted file mode 100644 index 9874a3a0697d2..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.tsx +++ /dev/null @@ -1,75 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { Option, none, some, fold, filter } from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { DocLinksStart, HttpSetup } from 'kibana/public'; - -import { AlertingFrameworkHealth } from '../../types'; -import { health } from '../lib/alert_api'; - -interface Props { - docLinks: Pick; - http: HttpSetup; -} - -export const SecurityEnabledCallOut: React.FunctionComponent = ({ docLinks, http }) => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; - - const [alertingHealth, setAlertingHealth] = React.useState>(none); - - React.useEffect(() => { - async function fetchSecurityConfigured() { - setAlertingHealth(some(await health({ http }))); - } - - fetchSecurityConfigured(); - }, [http]); - - return pipe( - alertingHealth, - filter(healthCheck => !healthCheck?.isSufficientlySecure), - fold( - () => , - () => ( - - -

- -

- - - -
- -
- ) - ) - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 7c8d798984bf2..4d0a9980f2231 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -29,8 +29,8 @@ import { hasShowActionsCapability, hasShowAlertsCapability } from './lib/capabil import { ActionsConnectorsList } from './sections/actions_connectors_list/components/actions_connectors_list'; import { AlertsList } from './sections/alerts_list/components/alerts_list'; -import { SecurityEnabledCallOut } from './components/security_call_out'; import { PLUGIN } from './constants/plugin'; +import { HealthCheck } from './components/health_check'; interface MatchParams { section: Section; @@ -88,7 +88,6 @@ export const TriggersActionsUIHome: React.FunctionComponent - @@ -142,9 +141,27 @@ export const TriggersActionsUIHome: React.FunctionComponent {canShowActions && ( - + ( + + + + )} + /> + )} + {canShowAlerts && ( + ( + + + + )} + /> )} - {canShowAlerts && } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts index ee68b7e269c34..e9cf2a270d180 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts @@ -43,27 +43,14 @@ describe('loadActionTypes', () => { }); describe('loadAllActions', () => { - test('should call find actions API', async () => { - const resolvedValue = { - page: 1, - perPage: 10000, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); + test('should call getAll actions API', async () => { + http.get.mockResolvedValueOnce([]); const result = await loadAllActions({ http }); - expect(result).toEqual(resolvedValue); + expect(result).toEqual([]); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/action/_find", - Object { - "query": Object { - "per_page": 10000, - "sort_field": "name.keyword", - "sort_order": "asc", - }, - }, + "/api/action/_getAll", ] `); }); @@ -73,6 +60,7 @@ describe('createActionConnector', () => { test('should call create action API', async () => { const connector: ActionConnectorWithoutId = { actionTypeId: 'test', + isPreconfigured: false, name: 'My test', config: {}, secrets: {}, @@ -86,7 +74,7 @@ describe('createActionConnector', () => { Array [ "/api/action", Object { - "body": "{\\"actionTypeId\\":\\"test\\",\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}", + "body": "{\\"actionTypeId\\":\\"test\\",\\"isPreconfigured\\":false,\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}", }, ] `); @@ -98,6 +86,7 @@ describe('updateActionConnector', () => { const id = '123'; const connector: ActionConnectorWithoutId = { actionTypeId: 'test', + isPreconfigured: false, name: 'My test', config: {}, secrets: {}, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts index 26ad97f05849d..e82d268accdd8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts @@ -8,32 +8,12 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ACTION_API_PATH } from '../constants'; import { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types'; -// We are assuming there won't be many actions. This is why we will load -// all the actions in advance and assume the total count to not go over 100 or so. -// We'll set this max setting assuming it's never reached. -const MAX_ACTIONS_RETURNED = 10000; - export async function loadActionTypes({ http }: { http: HttpSetup }): Promise { return await http.get(`${BASE_ACTION_API_PATH}/types`); } -export async function loadAllActions({ - http, -}: { - http: HttpSetup; -}): Promise<{ - page: number; - perPage: number; - total: number; - data: ActionConnector[]; -}> { - return await http.get(`${BASE_ACTION_API_PATH}/_find`, { - query: { - per_page: MAX_ACTIONS_RETURNED, - sort_field: 'name.keyword', - sort_order: 'asc', - }, - }); +export async function loadAllActions({ http }: { http: HttpSetup }): Promise { + return await http.get(`${BASE_ACTION_API_PATH}/_getAll`); } export async function createActionConnector({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 89d37c4d00a11..41564146bb84d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -11,6 +11,10 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import { ActionForm } from './action_form'; +jest.mock('../../lib/action_connector_api', () => ({ + loadAllActions: jest.fn(), + loadActionTypes: jest.fn(), +})); const actionTypeRegistry = actionTypeRegistryMock.create(); describe('action_form', () => { let deps: any; @@ -73,6 +77,17 @@ describe('action_form', () => { let wrapper: ReactWrapper; async function setup() { + const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); + loadAllActions.mockResolvedValueOnce([ + { + secrets: {}, + id: 'test', + actionTypeId: actionType.id, + name: 'Test connector', + config: {}, + isPreconfigured: false, + }, + ]); const mockes = coreMock.createSetup(); deps = { toastNotifications: mockes.notifications.toasts, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 3ade4e6368f96..6b011ac84bc6f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -129,7 +129,7 @@ export const ActionForm = ({ async function loadConnectors() { try { const actionsResponse = await loadAllActions({ http }); - setConnectors(actionsResponse.data); + setConnectors(actionsResponse); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index f9aa2cad8bfc6..2c063ea6b4fa6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -47,6 +47,7 @@ describe('connector_edit_flyout', () => { actionTypeId: 'test-action-type-id', actionType: 'test-action-type-name', name: 'action-connector', + isPreconfigured: false, referencedByCount: 0, config: {}, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts index df7e5d8fe9a78..e469a50108912 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts @@ -15,6 +15,7 @@ describe('connector reducer', () => { actionTypeId: 'test-action-type-id', name: 'action-connector', referencedByCount: 0, + isPreconfigured: false, config: {}, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss index 3d65b8a799b1b..70ad1cae6c1d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss @@ -1,7 +1,3 @@ -.actConnectorsList__logo + .actConnectorsList__logo { - margin-left: $euiSize; -} - .actConnectorsList__tableRowDisabled { background-color: $euiColorLightestShade; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 9331fe1704694..4fa1e7e4c6e4d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -29,12 +29,7 @@ describe('actions_connectors_list component empty', () => { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValueOnce([]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -111,27 +106,22 @@ describe('actions_connectors_list component with items', () => { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - config: {}, - }, - ], - }); + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -214,12 +204,7 @@ describe('actions_connectors_list component empty with show only capability', () const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValueOnce([]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -289,27 +274,22 @@ describe('actions_connectors_list with show only capability', () => { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - config: {}, - }, - ], - }); + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -384,27 +364,22 @@ describe('actions_connectors_list component with disabled items', () => { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - config: {}, - }, - ], - }); + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index fc07171347e5e..47e058f473946 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -10,9 +10,6 @@ import { EuiInMemoryTable, EuiSpacer, EuiButton, - EuiIcon, - EuiEmptyPrompt, - EuiTitle, EuiLink, EuiLoadingSpinner, EuiIconTip, @@ -30,6 +27,7 @@ import { ActionsConnectorsContextProvider } from '../../../context/actions_conne import { checkActionTypeEnabled } from '../../../lib/check_action_type_enabled'; import './actions_connectors_list.scss'; import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; +import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt'; export const ActionsConnectorsList: React.FunctionComponent = () => { const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); @@ -110,7 +108,7 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { setIsLoadingActions(true); try { const actionsResponse = await loadAllActions({ http }); - setActions(actionsResponse.data); + setActions(actionsResponse); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -324,51 +322,6 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { /> ); - const emptyPrompt = ( - - - - - - -

- -

-
-
- } - body={ -

- -

- } - actions={ - setAddFlyoutVisibility(true)} - > - - - } - /> - ); - const noPermissionPrompt = (

{ )} {data.length !== 0 && table} - {data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && emptyPrompt} + {data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && ( + setAddFlyoutVisibility(true)} /> + )} {data.length === 0 && !canSave && noPermissionPrompt} { docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; - mockes.http.get.mockResolvedValue({ isSufficientlySecure: true }); + mockes.http.get.mockResolvedValue({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); const alertType = { id: 'my-alert-type', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index e44e20751b315..0620ced6365a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback, useReducer, useState } from 'react'; +import { isObject } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -24,7 +25,7 @@ import { Alert, AlertAction, IErrorObject } from '../../../types'; import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; -import { AlertActionSecurityCallOut } from '../../components/alert_action_security_call_out'; +import { HealthCheck } from '../../components/health_check'; import { PLUGIN } from '../../constants/plugin'; interface AlertAddProps { @@ -83,7 +84,7 @@ export const AlertAdd = ({ ...(alertType ? alertType.validate(alert.params).errors : []), ...validateBaseProperties(alert).errors, } as IErrorObject; - const hasErrors = !!Object.keys(errors).find(errorKey => errors[errorKey].length >= 1); + const hasErrors = parseErrors(errors); const actionsErrors: Array<{ errors: IErrorObject; @@ -153,63 +154,61 @@ export const AlertAdd = ({

- - - - - - - - - {i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - - { - setIsSaving(true); - const savedAlert = await onSaveAlert(); - setIsSaving(false); - if (savedAlert) { - closeFlyout(); - if (reloadAlerts) { - reloadAlerts(); + + + + + + + + + {i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} + + + + { + setIsSaving(true); + const savedAlert = await onSaveAlert(); + setIsSaving(false); + if (savedAlert) { + closeFlyout(); + if (reloadAlerts) { + reloadAlerts(); + } } - } - }} - > - - - - - + }} + > + + + + + + ); }; + +const parseErrors: (errors: IErrorObject) => boolean = errors => + !!Object.values(errors).find(errorList => { + if (isObject(errorList)) return parseErrors(errorList as IErrorObject); + return errorList.length >= 1; + }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 6fcfb463c4c77..916ba368e0732 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -36,7 +36,10 @@ describe('alert_edit', () => { docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; - mockedCoreSetup.http.get.mockResolvedValue({ isSufficientlySecure: true }); + mockedCoreSetup.http.get.mockResolvedValue({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); const alertType = { id: 'my-alert-type', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 3f27a7860bafa..4255eca83be47 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -26,7 +26,7 @@ import { Alert, AlertAction, IErrorObject } from '../../../types'; import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; -import { AlertActionSecurityCallOut } from '../../components/alert_action_security_call_out'; +import { HealthCheck } from '../../components/health_check'; import { PLUGIN } from '../../constants/plugin'; interface AlertEditProps { @@ -137,77 +137,69 @@ export const AlertEdit = ({ - - - {hasActionsDisabled && ( - - - - - )} - - - - - - - {i18n.translate('xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - - { - setIsSaving(true); - const savedAlert = await onSaveAlert(); - setIsSaving(false); - if (savedAlert) { - closeFlyout(); - if (reloadAlerts) { - reloadAlerts(); - } - } - }} - > - + + {hasActionsDisabled && ( + + - - - - + +
+ )} + + + + + + + {i18n.translate('xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} + + + + { + setIsSaving(true); + const savedAlert = await onSaveAlert(); + setIsSaving(false); + if (savedAlert) { + closeFlyout(); + if (reloadAlerts) { + reloadAlerts(); + } + } + }} + > + + + + + + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 108cc724aa407..66aa02e1930a3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -72,12 +72,7 @@ describe('alerts_list component empty', () => { }, ]); loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); - loadAllActions.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ @@ -196,12 +191,7 @@ describe('alerts_list component with items', () => { }, ]); loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); - loadAllActions.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ { @@ -286,12 +276,7 @@ describe('alerts_list component empty with show only capability', () => { }, ]); loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); - loadAllActions.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ { @@ -405,12 +390,7 @@ describe('alerts_list with show only capability', () => { }, ]); loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); - loadAllActions.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index afd3299f0c2bb..5d59180ff572b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -15,7 +15,6 @@ import { EuiFlexItem, EuiIcon, EuiSpacer, - EuiEmptyPrompt, EuiLink, EuiLoadingSpinner, } from '@elastic/eui'; @@ -36,6 +35,7 @@ import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; +import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; const ENTER_KEY = 13; @@ -292,44 +292,6 @@ export const AlertsList: React.FunctionComponent = () => { ); } - const emptyPrompt = ( - - - - } - body={ -

- -

- } - actions={ - setAlertFlyoutVisibility(true)} - > - - - } - /> - ); - const table = ( @@ -473,7 +435,7 @@ export const AlertsList: React.FunctionComponent = () => { ) : ( - emptyPrompt + setAlertFlyoutVisibility(true)} /> )} void; fields: Record; customAggTypesOptions?: { diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index fb3ff9ceb0926..99dd7b63383fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -18,11 +18,12 @@ import { } from '@elastic/eui'; import { builtInComparators } from '../constants'; import { Comparator } from '../types'; +import { IErrorObject } from '../../types'; import { ClosablePopoverTitle } from './components'; interface ThresholdExpressionProps { thresholdComparator: string; - errors: { [key: string]: string[] }; + errors: IErrorObject; onChangeSelectedThresholdComparator: (selectedThresholdComparator?: string) => void; onChangeSelectedThreshold: (selectedThreshold?: number[]) => void; customComparators?: { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 7dfaa7b918f70..7f78d327d0122 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -66,6 +66,7 @@ export interface ActionConnector { name: string; referencedByCount?: number; config: Record; + isPreconfigured: boolean; } export type ActionConnectorWithoutId = Omit; @@ -111,5 +112,5 @@ export interface AlertTypeModel { } export interface IErrorObject { - [key: string]: string[]; + [key: string]: string | string[] | IErrorObject; } diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 7943da07716a1..061c9e4a0d921 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -43,6 +43,8 @@ const onlyNotInCoverageTests = [ require.resolve('../test/licensing_plugin/config.ts'), require.resolve('../test/licensing_plugin/config.public.ts'), require.resolve('../test/licensing_plugin/config.legacy.ts'), + require.resolve('../test/functional_endpoint_ingest_failure/config.ts'), + require.resolve('../test/functional_endpoint/config.ts'), ]; require('@kbn/plugin-helpers').babelRegister(); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 5fb1afa7d584f..4d32a5ae9f53c 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -77,6 +77,30 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', + `--xpack.actions.preconfigured=${JSON.stringify([ + { + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }, + { + id: 'custom-system-abc-connector', + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + secrets: { + xyzSecret1: 'credential1', + xyzSecret2: 'credential2', + }, + }, + ])}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, 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 e228f6c1f81c6..6001dd531cfae 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 @@ -39,6 +39,7 @@ export default function emailTest({ getService }: FtrProviderContext) { createdActionId = createdAction.id; expect(createdAction).to.eql({ id: createdActionId, + isPreconfigured: false, name: 'An email action', actionTypeId: '.email', config: { @@ -58,6 +59,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'An email action', actionTypeId: '.email', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index 01eaf92da33fe..612eba858ea0b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -40,6 +40,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'An index action', actionTypeId: '.index', config: { @@ -57,6 +58,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'An index action', actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null }, @@ -79,6 +81,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdActionWithIndex).to.eql({ id: createdActionWithIndex.id, + isPreconfigured: false, name: 'An index action with index config', actionTypeId: '.index', config: { @@ -96,6 +99,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedActionWithIndex).to.eql({ id: fetchedActionWithIndex.id, + isPreconfigured: false, name: 'An index action with index config', actionTypeId: '.index', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index cfc04663c6a4f..eeb0818b5fbab 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -50,6 +50,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A pagerduty action', actionTypeId: '.pagerduty', config: { @@ -65,6 +66,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A pagerduty action', actionTypeId: '.pagerduty', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts index f4ea568cf08c3..e9d3e6c542442 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts @@ -31,6 +31,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) { serverLogActionId = createdAction.id; expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A server.log action', actionTypeId: '.server-log', config: {}, @@ -44,6 +45,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A server.log action', actionTypeId: '.server-log', config: {}, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 48f348e1b834d..054f8f6141817 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -101,6 +101,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { @@ -117,6 +118,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 8afa43bfea21e..e00589b7e85b7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -47,6 +47,7 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A slack action', actionTypeId: '.slack', config: {}, @@ -60,6 +61,7 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A slack action', actionTypeId: '.slack', config: {}, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index da83dbf8c47e2..fd996ea4507ba 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -92,6 +92,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A generic Webhook action', actionTypeId: '.webhook', config: { @@ -108,6 +109,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A generic Webhook action', actionTypeId: '.webhook', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts index 43a3861491467..922315eba5a5c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts @@ -55,6 +55,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, response.body.id, 'action'); expect(response.body).to.eql({ id: response.body.id, + isPreconfigured: false, name: 'My action', actionTypeId: 'test.index-record', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts index 6fca330887c3e..011e47cf11b39 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts @@ -137,6 +137,36 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`shouldn't delete action from preconfigured list`, async () => { + const response = await supertestWithoutAuth + .delete(`${getUrlPrefix(space.id)}/api/action/my-slack1`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo'); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'Preconfigured action my-slack1 is not allowed to delete.', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index bed4c805aaf57..c84b089d48c85 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -59,6 +59,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAction.id, + isPreconfigured: false, actionTypeId: 'test.index-record', name: 'My action', config: { @@ -115,6 +116,40 @@ export default function getActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle get preconfigured action request appropriately', async () => { + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/action/my-slack1`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack#xyz', + isPreconfigured: true, + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts similarity index 57% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 89c5b4f451f82..80b512f3fb5e3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -10,11 +10,11 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/l import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function findActionTests({ getService }: FtrProviderContext) { +export default function getAllActionTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('find', () => { + describe('getAll', () => { const objectRemover = new ObjectRemover(supertest); afterEach(() => objectRemover.removeAll()); @@ -22,7 +22,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; describe(scenario.id, () => { - it('should handle find action request appropriately', async () => { + it('should handle get all action request appropriately', async () => { const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') @@ -40,11 +40,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix( - space.id - )}/api/action/_find?search=test.index-record&search_fields=actionTypeId` - ) + .get(`${getUrlPrefix(space.id)}/api/action/_getAll`) .auth(user.username, user.password); switch (scenario.id) { @@ -61,90 +57,47 @@ export default function findActionTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - page: 1, - perPage: 20, - total: 1, - data: [ - { - id: createdAction.id, - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - referencedByCount: 0, + expect(response.body).to.eql([ + { + id: createdAction.id, + isPreconfigured: false, + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, }, - ], - }); - break; - default: - throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); - } - }); - - it('should handle find action request with filter appropriately', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/action`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - objectRemover.add(space.id, createdAction.id, 'action'); - - const response = await supertestWithoutAuth - .get( - `${getUrlPrefix( - space.id - )}/api/action/_find?filter=action.attributes.actionTypeId:test.index-record` - ) - .auth(user.username, user.password); - - switch (scenario.id) { - case 'no_kibana_privileges at space1': - case 'space_1_all at space2': - expect(response.statusCode).to.eql(404); - expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }); - break; - case 'global_read at space1': - case 'superuser at space1': - case 'space_1_all at space1': - expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - page: 1, - perPage: 20, - total: 1, - data: [ - { - id: createdAction.id, - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - referencedByCount: 0, + referencedByCount: 0, + }, + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, - ], - }); + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); - it('should handle find request appropriately with proper referencedByCount', async () => { + it('should handle get all request appropriately with proper referencedByCount', async () => { const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') @@ -172,6 +125,13 @@ export default function findActionTests({ getService }: FtrProviderContext) { id: createdAction.id, params: {}, }, + { + group: 'default', + id: 'my-slack1', + params: { + message: 'test', + }, + }, ], }) ) @@ -179,11 +139,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAlert.id, 'alert'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix( - space.id - )}/api/action/_find?filter=action.attributes.actionTypeId:test.index-record` - ) + .get(`${getUrlPrefix(space.id)}/api/action/_getAll`) .auth(user.username, user.password); switch (scenario.id) { @@ -200,29 +156,47 @@ export default function findActionTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - page: 1, - perPage: 20, - total: 1, - data: [ - { - id: createdAction.id, - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - referencedByCount: 1, + expect(response.body).to.eql([ + { + id: createdAction.id, + isPreconfigured: false, + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, }, - ], - }); + referencedByCount: 1, + }, + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + referencedByCount: 1, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); - it(`shouldn't find action from another space`, async () => { + it(`shouldn't get actions from another space`, async () => { const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') @@ -240,11 +214,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix( - 'other' - )}/api/action/_find?search=test.index-record&search_fields=actionTypeId` - ) + .get(`${getUrlPrefix('other')}/api/action/_getAll`) .auth(user.username, user.password); switch (scenario.id) { @@ -261,12 +231,30 @@ export default function findActionTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - page: 1, - perPage: 20, - total: 0, - data: [], - }); + expect(response.body).to.eql([ + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index c6960a4eedd25..d7ec2e78ccb30 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -19,7 +19,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./execute')); - loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts index a792efede07ee..6cafbeb8c6ea8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts @@ -69,6 +69,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAction.id, + isPreconfigured: false, actionTypeId: 'test.index-record', name: 'My action updated', config: { @@ -307,6 +308,45 @@ export default function updateActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`shouldn't update action from preconfigured list`, async () => { + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/action/custom-system-abc-connector`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts index 3713e9c24419f..874d42ac04736 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts @@ -38,6 +38,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'An index action', actionTypeId: '.index', config: { @@ -55,6 +56,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'An index action', actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null }, @@ -77,6 +79,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdActionWithIndex).to.eql({ id: createdActionWithIndex.id, + isPreconfigured: false, name: 'An index action with index config', actionTypeId: '.index', config: { @@ -94,6 +97,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedActionWithIndex).to.eql({ id: fetchedActionWithIndex.id, + isPreconfigured: false, name: 'An index action with index config', actionTypeId: '.index', config: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts index efd707b59cd34..c70c289194abb 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts @@ -37,6 +37,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, response.body.id, 'action'); expect(response.body).to.eql({ id: response.body.id, + isPreconfigured: false, name: 'My action', actionTypeId: 'test.index-record', config: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts index 283e51352c272..26a811d2cc512 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts @@ -76,5 +76,16 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { message: 'Saved object [action/2] not found', }); }); + + it(`shouldn't delete action from preconfigured list`, async () => { + await supertest + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/action/my-slack1`) + .set('kbn-xsrf', 'foo') + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: `Preconfigured action my-slack1 is not allowed to delete.`, + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/find.ts deleted file mode 100644 index acbc9edc1f2fb..0000000000000 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/find.ts +++ /dev/null @@ -1,92 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default function findActionTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - describe('find', () => { - const objectRemover = new ObjectRemover(supertest); - - afterEach(() => objectRemover.removeAll()); - - it('should handle find action request appropriately', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); - - await supertest - .get( - `${getUrlPrefix( - Spaces.space1.id - )}/api/action/_find?search=test.index-record&search_fields=actionTypeId` - ) - .expect(200, { - page: 1, - perPage: 20, - total: 1, - data: [ - { - id: createdAction.id, - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - referencedByCount: 0, - }, - ], - }); - }); - - it(`shouldn't find action from another space`, async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); - - await supertest - .get( - `${getUrlPrefix( - Spaces.other.id - )}/api/action/_find?search=test.index-record&search_fields=actionTypeId` - ) - .expect(200, { - page: 1, - perPage: 20, - total: 0, - data: [], - }); - }); - }); -} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index 0f896bfaa0af9..a4a13441fb766 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -38,6 +38,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { .get(`${getUrlPrefix(Spaces.space1.id)}/api/action/${createdAction.id}`) .expect(200, { id: createdAction.id, + isPreconfigured: false, actionTypeId: 'test.index-record', name: 'My action', config: { @@ -71,5 +72,17 @@ export default function getActionTests({ getService }: FtrProviderContext) { message: `Saved object [action/${createdAction.id}] not found`, }); }); + + it('should handle get action request from preconfigured list', async () => { + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/action/my-slack1`).expect(200, { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts new file mode 100644 index 0000000000000..517c64f178af5 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Spaces } from '../../scenarios'; +import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function getAllActionTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('getAll', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + it('should handle get all action request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); + + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/action/_getAll`).expect(200, [ + { + id: createdAction.id, + isPreconfigured: false, + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + referencedByCount: 0, + }, + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); + }); + + it(`shouldn't get all action from another space`, async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); + + await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/action/_getAll`).expect(200, [ + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index fb2be8c86f4e8..75544b7fd4169 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -11,7 +11,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { describe('Actions', () => { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts index 18a0ecc23c1e1..2593f342a8a86 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts @@ -63,6 +63,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) actionTypeId: 'test.not-enabled', config: {}, id: 'uuid-actionId', + isPreconfigured: false, name: 'an action created before test.not-enabled was disabled', }); }); @@ -89,6 +90,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) actionTypeId: 'test.not-enabled', config: {}, id: 'uuid-actionId', + isPreconfigured: false, name: 'an action created before test.not-enabled was disabled', }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts index fb0c5e13c0720..05d26aaaed2ec 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts @@ -48,6 +48,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { }) .expect(200, { id: createdAction.id, + isPreconfigured: false, actionTypeId: 'test.index-record', name: 'My action updated', config: { @@ -99,5 +100,25 @@ export default function updateActionTests({ getService }: FtrProviderContext) { message: `Saved object [action/${createdAction.id}] not found`, }); }); + + it(`shouldn't update action from preconfigured list`, async () => { + await supertest + .put(`${getUrlPrefix(Spaces.space1.id)}/api/action/custom-system-abc-connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index bcba156a64f0e..4e21faa610bfe 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -9,10 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); - // ML tests need to be disabled in orde to get the ES snapshot with - // https://github.com/elastic/elasticsearch/pull/54713 promoted - // and should be re-enabled as part of https://github.com/elastic/kibana/pull/61980 - describe.skip('Machine Learning', function() { + describe('Machine Learning', function() { this.tags(['mlqa']); before(async () => { diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 77293ddff3f9f..9bec3fd076e86 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -41,7 +41,7 @@ export default function({ getService }: FtrProviderContext) { }, global: ['all', 'read'], space: ['all', 'read'], - reserved: ['ml', 'monitoring'], + reserved: ['ml_user', 'ml_admin', 'monitoring'], }; await supertest diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 0b29fc1cac7de..1f9eac148b302 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -39,7 +39,7 @@ export default function({ getService }: FtrProviderContext) { }, global: ['all', 'read'], space: ['all', 'read'], - reserved: ['ml', 'monitoring'], + reserved: ['ml_user', 'ml_admin', 'monitoring'], }; await supertest diff --git a/x-pack/test/functional/apps/machine_learning/index.ts b/x-pack/test/functional/apps/machine_learning/index.ts index 143899a61ffb9..47c699e309491 100644 --- a/x-pack/test/functional/apps/machine_learning/index.ts +++ b/x-pack/test/functional/apps/machine_learning/index.ts @@ -8,10 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); - // ML tests need to be disabled in orde to get the ES snapshot with - // https://github.com/elastic/elasticsearch/pull/54713 promoted - // and should be re-enabled as part of https://github.com/elastic/kibana/pull/61980 - describe.skip('machine learning', function() { + describe('machine learning', function() { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/functional/config.edge.js b/x-pack/test/functional/config.edge.js new file mode 100644 index 0000000000000..882fb6fea3686 --- /dev/null +++ b/x-pack/test/functional/config.edge.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default async function({ readConfigFile }) { + const chromeConfig = await readConfigFile(require.resolve('./config')); + + return { + ...chromeConfig.getAll(), + + browser: { + type: 'msedge', + }, + + junit: { + reportName: 'MS Chromium Edge XPack UI Functional Tests', + }, + }; +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index cff555feace18..bc9a67da731cc 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -57,7 +57,6 @@ export default async function({ readConfigFile }) { resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), resolve(__dirname, './apps/transform'), - resolve(__dirname, './apps/endpoint'), // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), ], @@ -88,7 +87,6 @@ export default async function({ readConfigFile }) { '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', '--telemetry.banner=false', '--timelion.ui.enabled=true', - '--xpack.endpoint.enabled=true', ], }, uiSettings: { @@ -199,9 +197,6 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana/', hash: '/management/elasticsearch/transform', }, - endpoint: { - pathname: '/app/endpoint', - }, }, // choose where esArchiver should load archives from diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json index 64dc395ab69a4..7068c24a4b26c 100644 --- a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json @@ -389,7 +389,8 @@ "type": "nested" }, "file_extension": { - "type": "long" + "ignore_above": 1024, + "type": "keyword" }, "project_file": { "properties": { diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 07c5719ae53c5..782d57adea770 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -46,8 +46,6 @@ import { LensPageProvider } from './lens_page'; import { InfraMetricExplorerProvider } from './infra_metric_explorer'; import { RoleMappingsPageProvider } from './role_mappings_page'; import { SpaceSelectorPageProvider } from './space_selector_page'; -import { EndpointPageProvider } from './endpoint_page'; -import { EndpointAlertsPageProvider } from './endpoint_alerts_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -81,6 +79,4 @@ export const pageObjects = { copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider, lens: LensPageProvider, roleMappings: RoleMappingsPageProvider, - endpoint: EndpointPageProvider, - endpointAlerts: EndpointAlertsPageProvider, }; diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index 36181b66786d5..af33ec2301edc 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -330,9 +330,11 @@ export function MachineLearningJobWizardCommonProvider( await this.ensureAdvancedSectionOpen(); subj = advancedSectionSelector(subj); } - await mlCommon.setValueWithChecks(subj, modelMemoryLimit, { clearWithKeyboard: true }); - await this.assertModelMemoryLimitValue(modelMemoryLimit, { - withAdvancedSection: sectionOptions.withAdvancedSection, + await retry.tryForTime(15 * 1000, async () => { + await mlCommon.setValueWithChecks(subj, modelMemoryLimit, { clearWithKeyboard: true }); + await this.assertModelMemoryLimitValue(modelMemoryLimit, { + withAdvancedSection: sectionOptions.withAdvancedSection, + }); }); }, diff --git a/x-pack/test/functional/services/transform_ui/source_selection.ts b/x-pack/test/functional/services/transform_ui/source_selection.ts index d2ef2c67f0004..38a819e285d67 100644 --- a/x-pack/test/functional/services/transform_ui/source_selection.ts +++ b/x-pack/test/functional/services/transform_ui/source_selection.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformSourceSelectionProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); return { async assertSourceListContainsEntry(sourceName: string) { @@ -23,8 +24,10 @@ export function TransformSourceSelectionProvider({ getService }: FtrProviderCont async selectSource(sourceName: string) { await this.filterSourceSelection(sourceName); - await testSubjects.clickWhenNotDisabled(`savedObjectTitle${sourceName}`); - await testSubjects.existOrFail('transformPageCreateTransform'); + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.clickWhenNotDisabled(`savedObjectTitle${sourceName}`); + await testSubjects.existOrFail('transformPageCreateTransform', { timeout: 10 * 1000 }); + }); }, }; } diff --git a/x-pack/test/functional/apps/endpoint/alerts.ts b/x-pack/test/functional_endpoint/apps/endpoint/alerts.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/alerts.ts rename to x-pack/test/functional_endpoint/apps/endpoint/alerts.ts diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts rename to x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/index.ts b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/index.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/feature_controls/index.ts rename to x-pack/test/functional_endpoint/apps/endpoint/feature_controls/index.ts diff --git a/x-pack/test/functional/apps/endpoint/header_nav.ts b/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/header_nav.ts rename to x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts diff --git a/x-pack/test/functional/apps/endpoint/host_list.ts b/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/host_list.ts rename to x-pack/test/functional_endpoint/apps/endpoint/host_list.ts diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional_endpoint/apps/endpoint/index.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/index.ts rename to x-pack/test/functional_endpoint/apps/endpoint/index.ts diff --git a/x-pack/test/functional/apps/endpoint/landing_page.ts b/x-pack/test/functional_endpoint/apps/endpoint/landing_page.ts similarity index 72% rename from x-pack/test/functional/apps/endpoint/landing_page.ts rename to x-pack/test/functional_endpoint/apps/endpoint/landing_page.ts index 65af91feae407..b4da4631aa60b 100644 --- a/x-pack/test/functional/apps/endpoint/landing_page.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/landing_page.ts @@ -7,8 +7,9 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default ({ getPageObjects }: FtrProviderContext) => { +export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'endpoint']); + const testSubjects = getService('testSubjects'); describe('Endpoint landing page', function() { this.tags('ciGroup7'); @@ -20,5 +21,9 @@ export default ({ getPageObjects }: FtrProviderContext) => { const welcomeEndpointMessage = await pageObjects.endpoint.welcomeEndpointTitle(); expect(welcomeEndpointMessage).to.be('Hello World'); }); + + it('Does not display a toast indicating that the ingest manager failed to initialize', async () => { + await testSubjects.missingOrFail('euiToastHeader'); + }); }); }; diff --git a/x-pack/test/functional/apps/endpoint/policy_list.ts b/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/policy_list.ts rename to x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts diff --git a/x-pack/test/functional_endpoint/config.ts b/x-pack/test/functional_endpoint/config.ts new file mode 100644 index 0000000000000..37bf57b67b47e --- /dev/null +++ b/x-pack/test/functional_endpoint/config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from './page_objects'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + return { + ...xpackFunctionalConfig.getAll(), + pageObjects, + testFiles: [resolve(__dirname, './apps/endpoint')], + junit: { + reportName: 'X-Pack Endpoint Functional Tests', + }, + apps: { + ...xpackFunctionalConfig.get('apps'), + endpoint: { + pathname: '/app/endpoint', + }, + }, + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.endpoint.enabled=true', + '--xpack.ingestManager.enabled=true', + '--xpack.ingestManager.fleet.enabled=true', + ], + }, + }; +} diff --git a/x-pack/test/functional_endpoint/ftr_provider_context.d.ts b/x-pack/test/functional_endpoint/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..21ab5d5a4e554 --- /dev/null +++ b/x-pack/test/functional_endpoint/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from './page_objects'; +import { services } from '../functional/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/functional/page_objects/endpoint_alerts_page.ts b/x-pack/test/functional_endpoint/page_objects/endpoint_alerts_page.ts similarity index 100% rename from x-pack/test/functional/page_objects/endpoint_alerts_page.ts rename to x-pack/test/functional_endpoint/page_objects/endpoint_alerts_page.ts diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional_endpoint/page_objects/endpoint_page.ts similarity index 100% rename from x-pack/test/functional/page_objects/endpoint_page.ts rename to x-pack/test/functional_endpoint/page_objects/endpoint_page.ts diff --git a/x-pack/test/functional_endpoint/page_objects/index.ts b/x-pack/test/functional_endpoint/page_objects/index.ts new file mode 100644 index 0000000000000..8138ce2eeccb3 --- /dev/null +++ b/x-pack/test/functional_endpoint/page_objects/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; +import { EndpointPageProvider } from './endpoint_page'; +import { EndpointAlertsPageProvider } from './endpoint_alerts_page'; + +export const pageObjects = { + ...xpackFunctionalPageObjects, + endpoint: EndpointPageProvider, + endpointAlerts: EndpointAlertsPageProvider, +}; diff --git a/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/index.ts b/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/index.ts new file mode 100644 index 0000000000000..ae35f3e525461 --- /dev/null +++ b/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/index.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('endpoint when the ingest manager fails to setup correctly', function() { + this.tags('ciGroup7'); + + loadTestFile(require.resolve('./landing_page')); + }); +} diff --git a/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/landing_page.ts b/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/landing_page.ts new file mode 100644 index 0000000000000..d29250ca3bed4 --- /dev/null +++ b/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/landing_page.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + describe('home page', function() { + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + before(async () => { + await pageObjects.common.navigateToApp('endpoint'); + }); + + it('displays an error toast', async () => { + await testSubjects.existOrFail('euiToastHeader'); + }); + }); +}; diff --git a/x-pack/test/functional_endpoint_ingest_failure/config.ts b/x-pack/test/functional_endpoint_ingest_failure/config.ts new file mode 100644 index 0000000000000..a2055a4bbdc18 --- /dev/null +++ b/x-pack/test/functional_endpoint_ingest_failure/config.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional_endpoint/config.ts') + ); + + return { + ...xpackFunctionalConfig.getAll(), + testFiles: [resolve(__dirname, './apps/endpoint')], + junit: { + reportName: 'X-Pack Endpoint Without Ingest Functional Tests', + }, + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + // use a bogus port so the ingest manager setup will fail + '--xpack.ingestManager.epm.registryUrl=http://127.0.0.1:12345', + ], + }, + }; +} diff --git a/x-pack/test/functional_endpoint_ingest_failure/ftr_provider_context.d.ts b/x-pack/test/functional_endpoint_ingest_failure/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..0e4b47471d419 --- /dev/null +++ b/x-pack/test/functional_endpoint_ingest_failure/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from '../functional_endpoint/page_objects'; +import { services } from '../functional/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 6c3c496da71f6..aa3c9bd24842a 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -14,7 +14,9 @@ import { pageObjects } from './page_objects'; /* eslint-disable import/no-default-export */ export default async function({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional_endpoint/config.ts') + ); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); diff --git a/yarn.lock b/yarn.lock index 77ab69c715573..3f04b2d26a013 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2499,6 +2499,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@sindresorhus/is@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-2.1.0.tgz#6ad4ca610f696098e92954ab431ff83bea0ce13f" + integrity sha512-lXKXfypKo644k4Da4yXkPCrwcvn6SlUW2X2zFbuflKHNjf0w9htru01bo26uMhleMXsDmnZ12eJLdrAZa9MANg== + "@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393" @@ -3398,6 +3403,13 @@ "@svgr/plugin-svgo" "^4.2.0" loader-utils "^1.2.3" +"@szmarczak/http-timer@^4.0.0": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + dependencies: + defer-to-connect "^2.0.0" + "@testim/chrome-version@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.0.7.tgz#0cd915785ec4190f08a3a6acc9b61fc38fb5f1a9" @@ -3646,6 +3658,16 @@ resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.0.tgz#d425c9818182ce71ce53866798cee9c7d41d6e53" integrity sha512-ZBvKzg3yyWNYEkwxAzdmUzp27sFvw+1m080/+2lwrt+eltNefn1f4fnpMyrjOla31p8zLleCYqQXw+3EETfn0w== +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -4015,6 +4037,11 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + "@types/indent-string@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/indent-string/-/indent-string-3.0.0.tgz#9ebb391ceda548926f5819ad16405349641b999f" @@ -4146,6 +4173,13 @@ dependencies: "@types/node" "*" +"@types/keyv@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + "@types/license-checker@15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/license-checker/-/license-checker-15.0.0.tgz#685d69e2cf61ffd862320434601f51c85e28bba1" @@ -4617,6 +4651,13 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/responselike@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" @@ -4632,10 +4673,10 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.5.tgz#23041a4948c82daf2df9836e4d2358fec10d3e24" - integrity sha512-ma1aL1znI3ptEbSQgbywgadrRCJouPIACSfOl/bPwu/TPNSyyE/+o9jZ6+bpDVTtIdksZuVKpq4SR1ip3DRduw== +"@types/selenium-webdriver@4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.9.tgz#12621e55b2ef8f6c98bd17fe23fa720c6cba16bd" + integrity sha512-HopIwBE7GUXsscmt/J0DhnFXLSmO04AfxT6b8HAprknwka7pqEWquWDMXxCjd+NUHK9MkCe1SDKKsMiNmCItbQ== "@types/semver@^5.5.0": version "5.5.0" @@ -7358,13 +7399,18 @@ binaryextensions@2: resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935" integrity sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA== -bindings@^1.5.0: +bindings@1, bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== dependencies: file-uri-to-path "1.0.0" +bindings@~1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" + integrity sha1-FK1hE4EtLTfXLme0ystLtyZQXxE= + bit-twiddle@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e" @@ -7898,7 +7944,7 @@ buffer@^5.1.0, buffer@^5.2.0: base64-js "^1.0.2" ieee754 "^1.1.4" -builtin-modules@^1.0.0: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= @@ -8045,6 +8091,13 @@ cache-loader@^4.1.0: neo-async "^2.6.1" schema-utils "^2.0.0" +cacheable-lookup@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-2.0.0.tgz#33b1e56f17507f5cf9bb46075112d65473fb7713" + integrity sha512-s2piO6LvA7xnL1AR03wuEdSx3BZT3tIJpZ56/lcJwzO/6DTJZlTs7X3lrvPxk6d1PlDe6PrVe2TjlUIZNFglAQ== + dependencies: + keyv "^4.0.0" + cacheable-request@^2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" @@ -8058,6 +8111,19 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + cachedir@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" @@ -8886,7 +8952,7 @@ clone-regexp@^1.0.0: is-regexp "^1.0.0" is-supported-regexp-flag "^1.0.0" -clone-response@1.0.2: +clone-response@1.0.2, clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -9150,16 +9216,16 @@ commander@4.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== +commander@^2.12.1, commander@^2.20.0, commander@^2.7.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -commander@^2.20.0, commander@^2.7.1: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^2.8.1: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -10575,7 +10641,7 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" -debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: +debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -10647,6 +10713,13 @@ decompress-response@^4.2.0: dependencies: mimic-response "^2.0.0" +decompress-response@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" + integrity sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw== + dependencies: + mimic-response "^2.0.0" + decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" @@ -10798,6 +10871,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" + integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -13239,6 +13317,17 @@ fetch-mock@^7.3.9: path-to-regexp "^2.2.1" whatwg-url "^6.5.0" +ffi@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/ffi/-/ffi-2.3.0.tgz#fa1a7b3d85c0fa8c83d96947a64b5192bc47f858" + integrity sha512-vkPA9Hf9CVuQ5HeMZykYvrZF2QNJ/iKGLkyDkisBnoOOFeFXZQhUPxBARPBIZMJVulvBI2R+jgofW03gyPpJcQ== + dependencies: + bindings "~1.2.0" + debug "2" + nan "2" + ref "1" + ref-struct "1" + figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -14716,6 +14805,27 @@ got@5.6.0: unzip-response "^1.0.0" url-parse-lax "^1.0.0" +got@^10.6.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-10.6.0.tgz#ac3876261a4d8e5fc4f81186f79955ce7b0501dc" + integrity sha512-3LIdJNTdCFbbJc+h/EH0V5lpNpbJ6Bfwykk21lcQvQsEcrzdi/ltCyQehFHLzJ/ka0UMH4Slg0hkYvAZN9qUDg== + dependencies: + "@sindresorhus/is" "^2.0.0" + "@szmarczak/http-timer" "^4.0.0" + "@types/cacheable-request" "^6.0.1" + cacheable-lookup "^2.0.0" + cacheable-request "^7.0.1" + decompress-response "^5.0.0" + duplexer3 "^0.1.4" + get-stream "^5.0.0" + lowercase-keys "^2.0.0" + mimic-response "^2.1.0" + p-cancelable "^2.0.0" + p-event "^4.0.0" + responselike "^2.0.0" + to-readable-stream "^2.0.0" + type-fest "^0.10.0" + got@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" @@ -15816,6 +15926,11 @@ http-cache-semantics@3.8.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -16854,6 +16969,11 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e" integrity sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g== +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + is-glob@4.0.0, is-glob@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" @@ -18124,6 +18244,11 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a" @@ -18359,7 +18484,7 @@ jsx-to-string@^1.4.0: json-stringify-pretty-compact "^1.0.1" react "^0.14.0" -jszip@^3.1.5: +jszip@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== @@ -18535,6 +18660,13 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" +keyv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.0.tgz#2d1dab694926b2d427e4c74804a10850be44c12f" + integrity sha512-U7ioE8AimvRVLfw4LffyOIRhL2xVgmE8T22L6i0BucSnBUyv4w+I7VN/zVZwRKHOI6ZRUcdMdWHQ8KSUvGpEog== + dependencies: + json-buffer "3.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -19503,6 +19635,11 @@ lowercase-keys@^1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lowlight@~1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.9.1.tgz#ed7c3dffc36f8c1f263735c0fe0c907847c11250" @@ -20163,6 +20300,11 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46" integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ== +mimic-response@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + mimos@4.x.x: version "4.0.0" resolved "https://registry.yarnpkg.com/mimos/-/mimos-4.0.0.tgz#76e3d27128431cb6482fd15b20475719ad626a5a" @@ -20591,6 +20733,19 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" +ms-chromium-edge-driver@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ms-chromium-edge-driver/-/ms-chromium-edge-driver-0.2.0.tgz#0e0c6fd9fd1d1d36db97b2b3d7e9d4ba4d2de456" + integrity sha512-RkDsBPnMLjRna7q4LlvtLb+CHPei9gZapnlxm3ayWKk3Ab6HmDsz/17xG2eyqkKX5UcKeo04YlLZ345tO7OolA== + dependencies: + extract-zip "^1.6.7" + got "^10.6.0" + lodash "4.17.15" + tslint "^6.1.0" + tslint-config-prettier "^1.18.0" + util "^0.12.2" + windows-registry "^0.1.5" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -20703,7 +20858,7 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.12.1, nan@^2.13.2: +nan@2, nan@^2.12.1, nan@^2.13.2: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -21206,6 +21361,11 @@ normalize-url@^3.3.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + now-and-later@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee" @@ -21891,6 +22051,11 @@ p-cancelable@^0.4.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== +p-cancelable@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" + integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -21903,7 +22068,7 @@ p-each-series@^1.0.0: dependencies: p-reduce "^1.0.0" -p-event@^4.1.0: +p-event@^4.0.0, p-event@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.1.0.tgz#e92bb866d7e8e5b732293b1c8269d38e9982bf8e" integrity sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA== @@ -24952,6 +25117,31 @@ redux@^4.0.5: loose-envify "^1.4.0" symbol-observable "^1.2.0" +ref-struct@1, ref-struct@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ref-struct/-/ref-struct-1.1.0.tgz#5d5ee65ad41cefc3a5c5feb40587261e479edc13" + integrity sha1-XV7mWtQc78Olxf60BYcmHkee3BM= + dependencies: + debug "2" + ref "1" + +ref-union@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ref-union/-/ref-union-1.0.1.tgz#3a2397f862f1e75171d687268f43b3f17729f120" + integrity sha1-OiOX+GLx51Fx1ocmj0Oz8Xcp8SA= + dependencies: + debug "2" + ref "1" + +ref@1, ref@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ref/-/ref-1.3.5.tgz#0e33f080cdb94a3d95312b2b3b1fd0f82044ca0f" + integrity sha512-2cBCniTtxcGUjDpvFfVpw323a83/0RLSGJJY5l5lcomZWhYpU2cuLdsvYqMixvsdLJ9+sTdzEkju8J8ZHDM2nA== + dependencies: + bindings "1" + debug "2" + nan "2" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -25691,6 +25881,13 @@ responselike@1.0.2: dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -26249,15 +26446,14 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= -selenium-webdriver@^4.0.0-alpha.5: - version "4.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.5.tgz#e4683b3dbf827d70df09a7e43bf02ebad20fa7c1" - integrity sha512-hktl3DSrhzM59yLhWzDGHIX9o56DvA+cVK7Dw6FcJR6qQ4CGzkaHeXQPcdrslkWMTeq0Ci9AmCxq0EMOvm2Rkg== +selenium-webdriver@^4.0.0-alpha.7: + version "4.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz#e3879d8457fd7ad8e4424094b7dc0540d99e6797" + integrity sha512-D4qnTsyTr91jT8f7MfN+OwY0IlU5+5FmlO5xlgRUV6hDEV8JyYx2NerdTEqDDkNq7RZDYc4VoPALk8l578RBHw== dependencies: - jszip "^3.1.5" - rimraf "^2.6.3" + jszip "^3.2.2" + rimraf "^2.7.1" tmp "0.0.30" - xml2js "^0.4.19" selfsigned@^1.10.7: version "1.10.7" @@ -28648,6 +28844,11 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" +to-readable-stream@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" + integrity sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -28927,6 +29128,37 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.2, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tslint-config-prettier@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" + integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== + +tslint@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.0.tgz#c6c611b8ba0eed1549bf5a59ba05a7732133d851" + integrity sha512-fXjYd/61vU6da04E505OZQGb2VCN2Mq3doeWcOIryuG+eqdmFUXTYVwdhnbEu2k46LNLgUYt9bI5icQze/j0bQ== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^4.0.1" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.10.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -29431,6 +29663,11 @@ type-detect@^1.0.0: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" integrity sha1-diIXzAbbJY7EiQihKY6LlRIejqI= +type-fest@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" + integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== + type-fest@^0.3.0, type-fest@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -30121,6 +30358,16 @@ util@^0.11.0: dependencies: inherits "2.0.3" +util@^0.12.2: + version "0.12.2" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.2.tgz#54adb634c9e7c748707af2bf5a8c7ab640cbba2b" + integrity sha512-XE+MkWQvglYa+IOfBt5UFG93EmncEMP23UqpgDvVZVFBPxwmkK10QRp6pgU4xICPnWRf/t0zPv4noYSUq9gqUQ== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + safe-buffer "^5.1.2" + utila@^0.4.0, utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -31277,6 +31524,17 @@ window-size@^0.2.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= +windows-registry@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/windows-registry/-/windows-registry-0.1.5.tgz#92c25c960884b0d215e69395f52d8dfaa0ba4ad0" + integrity sha512-gMN3ets1fbdP+TApEbbX2TIfBK3MIH5+p9GMvIFS3CNLr7U0Khe5mRj/T5zvwo/pKdhJgDrCLYyaNSs7HYiBCw== + dependencies: + debug "^2.2.0" + ffi "^2.0.0" + ref "^1.2.0" + ref-struct "^1.0.2" + ref-union "^1.0.0" + windows-release@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f" @@ -31575,14 +31833,6 @@ xml-parse-from-string@^1.0.0: resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" integrity sha1-qQKekp09vN7RafPG4oI42VpdWig= -xml2js@^0.4.19, xml2js@^0.4.5: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - xml2js@^0.4.22: version "0.4.22" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02" @@ -31592,6 +31842,14 @@ xml2js@^0.4.22: util.promisify "~1.0.0" xmlbuilder "~11.0.0" +xml2js@^0.4.5: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"