diff --git a/.eslintrc.js b/.eslintrc.js index 3c173e52440092..2ce6d279d93a9f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -69,26 +69,6 @@ module.exports = { 'jsx-a11y/no-onchange': 'off', }, }, - { - files: ['src/legacy/core_plugins/expressions/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, - { - files: [ - 'src/legacy/core_plugins/vis_default_editor/public/components/controls/**/*.{ts,tsx}', - ], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, - { - files: ['src/legacy/ui/public/vis/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/plugins/es_ui_shared/**/*.{js,ts,tsx}'], rules: { diff --git a/.i18nrc.json b/.i18nrc.json index c293b3103a39c8..19d361aed93445 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", @@ -43,7 +44,7 @@ "tileMap": "src/legacy/core_plugins/tile_map", "timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"], "uiActions": "src/plugins/ui_actions", - "visDefaultEditor": "src/legacy/core_plugins/vis_default_editor", + "visDefaultEditor": "src/plugins/vis_default_editor", "visTypeMarkdown": "src/legacy/core_plugins/vis_type_markdown", "visTypeMetric": "src/legacy/core_plugins/vis_type_metric", "visTypeTable": "src/legacy/core_plugins/vis_type_table", diff --git a/docs/apm/images/service-maps-java.png b/docs/apm/images/service-maps-java.png new file mode 100644 index 00000000000000..e1a42f4c76e12f Binary files /dev/null and b/docs/apm/images/service-maps-java.png differ diff --git a/docs/apm/images/service-maps.png b/docs/apm/images/service-maps.png new file mode 100644 index 00000000000000..454ae9bb720fbd Binary files /dev/null and b/docs/apm/images/service-maps.png differ diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc new file mode 100644 index 00000000000000..e0d84f33b4dcb9 --- /dev/null +++ b/docs/apm/service-maps.asciidoc @@ -0,0 +1,48 @@ +[[service-maps]] +=== Service maps + +beta::[] + +A service map is a real-time diagram of the interactions occurring in your application’s architecture. +It allows you to easily visualize data flow and high-level statistics, like average transaction duration, +requests per minute, errors per minute, and metrics, allowing you to quickly assess the status of your services. + +Our beta offering creates two types of service maps: + +* Global: All services and connections are shown. +* Service-specific: Selecting a specific service will highlight it's connections. + +[role="screenshot"] +image::apm/images/service-maps.png[Example view of service maps in the APM app in Kibana] + +[float] +[[visualize-your-architecture]] +=== Visualize your architecture + +Select the **Service Map** tab to get started. +By default, all services and connections are shown. +Whether your onboarding a new engineer, or just trying to grasp the big picture, +click around, zoom in and out, and begin to visualize how your services are connected. + +If there's a specific service that interests you, select that service to highlight its connections. +Clicking **Focus map** will refocus the map on that specific service and lock the connection highlighting. +From here, select **Service Details**, or click on the **Transaction** tab to jump to the Transaction overview. +You can also use the tabs at the top of the page to easily jump to the **Errors** or **Metrics** overview. + +While it's not possible to query in service maps, it is possible to filter by environment. +This can be useful if you have two or more services, in separate environments, but with the same name. +Use the environment drop down to only see the data you're interested in, like `dev` or `production`. + +[role="screenshot"] +image::apm/images/service-maps-java.png[Example view of service maps with Java highlighted in the APM app in Kibana] + +[float] +[[service-maps-legend]] +=== Legend + +Nodes appear on the map in one of two shapes: + +* **Circle**: Instrumented services. Interior icons are based on the language of the agent used. +* **Diamond**: Databases, external, and messaging. Interior icons represent the generic type, +with specific icons for known entities, like Elasticsearch. +Type and subtype are based on `span.type`, and `span.subtype`. diff --git a/docs/apm/using-the-apm-ui.asciidoc b/docs/apm/using-the-apm-ui.asciidoc index 1361dc046e3b1f..b1b7ed73079866 100644 --- a/docs/apm/using-the-apm-ui.asciidoc +++ b/docs/apm/using-the-apm-ui.asciidoc @@ -31,6 +31,8 @@ include::transactions.asciidoc[] include::spans.asciidoc[] +include::service-maps.asciidoc[] + include::errors.asciidoc[] include::metrics.asciidoc[] diff --git a/docs/developer/plugin/development-plugin-feature-registration.asciidoc b/docs/developer/plugin/development-plugin-feature-registration.asciidoc index ca61e5309ce85c..4702204196bf21 100644 --- a/docs/developer/plugin/development-plugin-feature-registration.asciidoc +++ b/docs/developer/plugin/development-plugin-feature-registration.asciidoc @@ -45,10 +45,15 @@ Registering a feature consists of the following fields. For more information, co |An array of applications this feature enables. Typically, all of your plugin's apps (from `uiExports`) will be included here. |`privileges` (required) -|{repo}blob/{branch}/x-pack/plugins/features/server/feature.ts[`FeatureWithAllOrReadPrivileges`]. +|{repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. |See <> and <> |The set of privileges this feature requires to function. +|`subFeatures` (optional) +|{repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. +|See <> +|The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher. + |`icon` |`string` |"discoverApp" @@ -192,3 +197,78 @@ server.route({ } }); ----------- + +[[example-3-discover]] +==== Example 3: Discover + +Discover takes advantage of subfeature privileges to allow fine-grained access control. In this example, +a single "Create Short URLs" subfeature privilege is defined, which allows users to grant access to this feature without having to grant the `all` privilege to Discover. In other words, you can grant `read` access to Discover, and also grant the ability to create short URLs. + +["source","javascript"] +----------- +init(server) { + const xpackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + { + id: 'discover', + name: i18n.translate('xpack.features.discoverFeatureName', { + defaultMessage: 'Discover', + }), + order: 100, + icon: 'discoverApp', + navLinkId: 'kibana:discover', + app: ['kibana'], + catalogue: ['discover'], + privileges: { + all: { + app: ['kibana'], + catalogue: ['discover'], + savedObject: { + all: ['search', 'query'], + read: ['index-pattern'], + }, + ui: ['show', 'save', 'saveQuery'], + }, + read: { + app: ['kibana'], + catalogue: ['discover'], + savedObject: { + all: [], + read: ['index-pattern', 'search', 'query'], + }, + ui: ['show'], + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.features.ossFeatures.discoverShortUrlSubFeatureName', { + defaultMessage: 'Short URLs', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'url_create', + name: i18n.translate( + 'xpack.features.ossFeatures.discoverCreateShortUrlPrivilegeName', + { + defaultMessage: 'Create Short URLs', + } + ), + includeIn: 'all', + savedObject: { + all: ['url'], + read: [], + }, + ui: ['createShortUrl'], + }, + ], + }, + ], + }, + ], + } + }); +} +----------- 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 29fdc37a811767..c10b460da8b4f7 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 00000000000000..f5ea627a9f008a --- /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 00000000000000..b41e7020c38e95 --- /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 00000000000000..3fde86a18c58bd --- /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 00000000000000..d554c6f70d7208 --- /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 00000000000000..f8a45fe9a5a9ce --- /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 00000000000000..2398410fa4b840 --- /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 00000000000000..7374ccd9e7fa87 --- /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 793684c1b37961..accab9bf0cb360 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 00000000000000..8e7298d28801c9 --- /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 00000000000000..82a4800a3b4b63 --- /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 00000000000000..347f2d3474b114 --- /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 00000000000000..6fcfacc3bc9085 --- /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 00000000000000..415a7825ee2bfe --- /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 00000000000000..6c017e9fc800ce --- /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 00000000000000..3a0b23d18632f1 --- /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 00000000000000..6a29623b2f122d --- /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 00000000000000..fa369aa0bdfbb4 --- /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 00000000000000..5ef8c1251a6024 --- /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 00000000000000..551c10c9bff820 --- /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 00000000000000..d35fc951c57ffb --- /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 00000000000000..a48994daa5a4e4 --- /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 00000000000000..db90afd6f74a65 --- /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 00000000000000..5f995ff5e13e3b --- /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 00000000000000..a66cec78c736b8 --- /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 00000000000000..6662e68b44d364 --- /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 00000000000000..0551a217520ad7 --- /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/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 259d725b3bf0d9..e756eb9b729050 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/security/authorization/kibana-privileges.asciidoc b/docs/user/security/authorization/kibana-privileges.asciidoc index 6a0b7cefab018b..21fcb9bdccb878 100644 --- a/docs/user/security/authorization/kibana-privileges.asciidoc +++ b/docs/user/security/authorization/kibana-privileges.asciidoc @@ -43,6 +43,10 @@ Assigning a feature privilege grants access to a specific feature. `all`:: Grants full read-write access. `read`:: Grants read-only access. +===== Sub-feature privileges +Some features allow for finer access control than the `all` and `read` privileges. +This additional level of control is available in the Gold subscription level and higher. + ===== Assigning feature privileges From the role management screen: @@ -62,7 +66,8 @@ PUT /api/security/role/my_kibana_role { "base": [], "feature": { - "dashboard": ["all"] + "visualize": ["all"], + "dashboard": ["read", "url_create"] }, "spaces": ["marketing"] } diff --git a/docs/user/security/images/assign_feature_privilege.png b/docs/user/security/images/assign_feature_privilege.png index e7d000d4554ad3..26fbbf20b39ad6 100644 Binary files a/docs/user/security/images/assign_feature_privilege.png and b/docs/user/security/images/assign_feature_privilege.png differ diff --git a/package.json b/package.json index 4c5db5321c2823..bd0fec3a5c116e 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 b648004760d7c7..b3e5a8c518682e 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 d52d89eebe2f13..4b4bb1282d9395 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 0cc1ad6326671d..7a858deff41d37 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 66f17ab579ec39..f4b91d154cbb88 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 d2305d643e4d20..1f1392b02baffa 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 e5cbffc3e2d943..b4e2c3095f14a7 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 63aba0dde2af81..bf7711055e97ba 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 389d98a0818c8e..da8846f6dddbb9 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 b92a6edf778ed0..684f6e15caff98 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 cfd72a6fd5e47b..2e45f710c4dcfd 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 00000000000000..dd5fb04bfd1c63 --- /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 00000000000000..1eaa338af12399 --- /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 ef8edecfd26ec6..3d38935e9fbf0f 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 3e760ec0efabd6..7bd6331978d1de 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 56ce16a951aa26..a298f80f96d8f0 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 825deea99bc230..ede0d3dc9fcc71 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 c6860086e77842..0cf2ebe55ea10d 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 bb5f6d5617aaea..f77230301ce023 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 31bf17da041afb..faf73044cac4d1 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 32662f07a86f02..61d97aea97459c 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 b50e47b9eab738..fe4795cad11a5a 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 4fbadf90f4b600..466d399f653cd6 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 ddd82edd934484..5ba2d0afc692e8 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 dc966f0797822c..8ddaed3707eb0c 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 25772c4c9b0b16..df4751521ac533 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 2ee656721abd0b..257b32c1e4c237 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 fd82bf282266ef..336eeff99f47b6 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 bc29061b380b88..dafd6c53411966 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 9fe32b14e64507..7ba4613c857d7b 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 aa440c6454569a..62027928c0bb5d 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 00000000000000..8efea1e2c00c65 --- /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 00000000000000..66a6e2baa17a72 --- /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 962965a08f8b2a..f14e9d9efb5e3d 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 e4e2b8d7adbb77..f3e3b7736d8d30 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 53d1b742a64944..5d535c98457249 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 a4b5a9d81df203..24c41d511180a1 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 222be572b75e49..07ea431dd3a0df 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 00000000000000..7516e82ee784de --- /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 00000000000000..748a54f0bf8bba --- /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/server/status/constants.js b/src/core/server/status/index.ts similarity index 90% rename from src/legacy/server/status/constants.js rename to src/core/server/status/index.ts index 3bb23749bae874..c39115d55a6827 100644 --- a/src/legacy/server/status/constants.js +++ b/src/core/server/status/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export const KIBANA_STATS_TYPE = 'oss_kibana_stats'; // kibana stats per 5s intervals +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 00000000000000..d550c2f06750bf --- /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 00000000000000..6d92a266369b90 --- /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 00000000000000..b6697d82219519 --- /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 00000000000000..765fa8771f3758 --- /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 00000000000000..84a7356c66bbf7 --- /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 470b1c2d135b70..f7e6fbcd0c131e 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 8bb3fb20cea8b7..ba3cd1280f34bb 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/components/editor/controls_tab.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx index 029e1f149d0520..b7114f1029ef2c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -30,7 +30,7 @@ import { EuiSelect, } from '@elastic/eui'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { IIndexPattern } from 'src/plugins/data/public'; import { ControlEditor } from './control_editor'; import { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx index 95b14619c34169..cdff6cabad8ba8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx @@ -23,7 +23,7 @@ import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSwitchEvent } from '@elastic/eui'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; interface OptionsTabParams { updateFiltersOnChange: boolean; 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 023e6ebb7125ce..badea68eec19f9 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/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 607d79b81618e9..6ccbc13aeeb571 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -201,7 +201,6 @@ function createDocTableModule() { .directive('docTable', createDocTableDirective) .directive('kbnTableHeader', createTableHeaderDirective) .directive('toolBarPagerText', createToolBarPagerTextDirective) - .directive('toolBarPagerText', createToolBarPagerTextDirective) .directive('kbnTableRow', createTableRowDirective) .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) .directive('kbnInfiniteScroll', createInfiniteScrollDirective) 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 55f369eaecd2c2..98679a8f24d16a 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,10 +52,6 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ setTrackedUrl: (url: string) => void; }>('urlTracker'); -// EXPORT legacy static dependencies, should be migrated when available in a new version; -export { wrapInI18nContext } from 'ui/i18n'; -import { search } from '../../../../../plugins/data/public'; -import { createGetterSetter } from '../../../../../plugins/kibana_utils/common'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; export { unhashUrl, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar.tsx index 57ad8e0b1040f3..8fcfcba08955c3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar.tsx @@ -18,7 +18,7 @@ */ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFieldNumber, @@ -88,77 +88,83 @@ export function ActionBar({ }; return ( -
- {isSuccessor && } - {isSuccessor && showWarning && } - {isSuccessor && showWarning && } - - - { - const value = newDocCount + defaultStepSize; - if (isValid(value)) { - setNewDocCount(value); - onChangeCount(value); - } - }} - flush="right" - > - - - - - - { - setNewDocCount(ev.target.valueAsNumber); - }} - onBlur={() => { - if (newDocCount !== docCount && isValid(newDocCount)) { - onChangeCount(newDocCount); + + + {isSuccessor && } + {isSuccessor && showWarning && ( + + )} + {isSuccessor && showWarning && } + + + { + const value = newDocCount + defaultStepSize; + if (isValid(value)) { + setNewDocCount(value); + onChangeCount(value); } }} - type="number" - value={newDocCount >= 0 ? newDocCount : ''} - /> - - - - - {isSuccessor ? ( - - ) : ( - + + + + + + { + setNewDocCount(ev.target.valueAsNumber); + }} + onBlur={() => { + if (newDocCount !== docCount && isValid(newDocCount)) { + onChangeCount(newDocCount); + } + }} + type="number" + value={newDocCount >= 0 ? newDocCount : ''} /> - )} - - - - {!isSuccessor && showWarning && } - {!isSuccessor && } - + + + + + {isSuccessor ? ( + + ) : ( + + )} + + + + {!isSuccessor && showWarning && ( + + )} + {!isSuccessor && } + + ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar_directive.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar_directive.ts index 697b039adde818..b705b4e4faeb59 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar_directive.ts @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { getAngularModule, wrapInI18nContext } from '../../../../../kibana_services'; +import { getAngularModule } from '../../../../../kibana_services'; import { ActionBar } from './action_bar'; getAngularModule().directive('contextActionBar', function(reactDirective: any) { - return reactDirective(wrapInI18nContext(ActionBar)); + return reactDirective(ActionBar); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js index 5482258e06564f..5a999235bf9a5c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js @@ -20,16 +20,12 @@ import { DiscoverNoResults } from './no_results'; import { DiscoverUninitialized } from './uninitialized'; import { DiscoverHistogram } from './histogram'; -import { getAngularModule, wrapInI18nContext } from '../../../kibana_services'; +import { getAngularModule } from '../../../kibana_services'; const app = getAngularModule(); -app.directive('discoverNoResults', reactDirective => - reactDirective(wrapInI18nContext(DiscoverNoResults)) -); +app.directive('discoverNoResults', reactDirective => reactDirective(DiscoverNoResults)); -app.directive('discoverUninitialized', reactDirective => - reactDirective(wrapInI18nContext(DiscoverUninitialized)) -); +app.directive('discoverUninitialized', reactDirective => reactDirective(DiscoverUninitialized)); app.directive('discoverHistogram', reactDirective => reactDirective(DiscoverHistogram)); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.js index ba02068590c149..ad81d5252a25c0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.js @@ -18,7 +18,7 @@ */ import React, { Component, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import PropTypes from 'prop-types'; import { @@ -247,29 +247,31 @@ export class DiscoverNoResults extends Component { } return ( - - + + + - - - - } - color="warning" - iconType="help" - data-test-subj="discoverNoResults" - /> + + + + } + color="warning" + iconType="help" + data-test-subj="discoverNoResults" + /> - {shardFailuresMessage} - {timeFieldMessage} - {luceneQueryMessage} - - - + {shardFailuresMessage} + {timeFieldMessage} + {luceneQueryMessage} + + + + ); } } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/uninitialized.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/uninitialized.tsx index f40865800098eb..b308607bbfbb07 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/uninitialized.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/uninitialized.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiButton, EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; @@ -28,38 +28,40 @@ interface Props { export const DiscoverUninitialized = ({ onRefresh }: Props) => { return ( - - - - - - - } - body={ -

- -

- } - actions={ - - - - } - /> -
-
-
+ + + + + + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> +
+
+
+
); }; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc.ts index 459dcfb30d17b9..092e3c79b1007c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { getAngularModule, wrapInI18nContext, getServices } from '../../kibana_services'; +import { getAngularModule, getServices } from '../../kibana_services'; // @ts-ignore import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; import html from './doc.html'; @@ -30,7 +30,7 @@ const { timefilter } = getServices(); const app = getAngularModule(); app.directive('discoverDoc', function(reactDirective: any) { return reactDirective( - wrapInI18nContext(Doc), + Doc, [ ['id', { watchDepth: 'value' }], ['index', { watchDepth: 'value' }], diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/index.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/index.ts index f21f3b17c69558..7e155a6b82ca08 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/index.ts @@ -16,14 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { wrapInI18nContext } from '../../../../../kibana_services'; import { ToolBarPagerText } from './tool_bar_pager_text'; import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; export function createToolBarPagerTextDirective(reactDirective: any) { - return reactDirective(wrapInI18nContext(ToolBarPagerText)); + return reactDirective(ToolBarPagerText); } export function createToolBarPagerButtonsDirective(reactDirective: any) { - return reactDirective(wrapInI18nContext(ToolBarPagerButtons)); + return reactDirective(ToolBarPagerButtons); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/tool_bar_pager_text.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/tool_bar_pager_text.tsx index 608dadd36b4b9a..84338d817c86b5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/tool_bar_pager_text.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/tool_bar_pager_text.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; interface Props { startItem: number; @@ -27,12 +27,14 @@ interface Props { export function ToolBarPagerText({ startItem, endItem, totalItems }: Props) { return ( -
- -
+ +
+ +
+
); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts index 84d865fd22a9a3..5e7ad6a1d1ea0b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts @@ -17,13 +17,13 @@ * under the License. */ import { TableHeader } from './table_header/table_header'; -import { wrapInI18nContext, getServices } from '../../../../kibana_services'; +import { getServices } from '../../../../kibana_services'; export function createTableHeaderDirective(reactDirective: any) { const { uiSettings: config } = getServices(); return reactDirective( - wrapInI18nContext(TableHeader), + TableHeader, [ ['columns', { watchDepth: 'collection' }], ['hideTimeColumn', { watchDepth: 'value' }], diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.tsx index 28a17dbdb67b7f..f3ceaef57d7008 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; @@ -65,83 +65,85 @@ export function Doc(props: DocProps) { const [reqState, hit, indexPattern] = useEsDocSearch(props); return ( - - {reqState === ElasticRequestState.NotFoundIndexPattern && ( - - } - /> - )} - {reqState === ElasticRequestState.NotFound && ( - - } - > - + + {reqState === ElasticRequestState.NotFoundIndexPattern && ( + + } /> - - )} - - {reqState === ElasticRequestState.Error && ( - + } + > - } - > - {' '} - + )} + + {reqState === ElasticRequestState.Error && ( + + } > - - - )} + id="kbn.doc.somethingWentWrongDescription" + defaultMessage="{indexName} is missing." + values={{ indexName: props.index }} + />{' '} + + + + + )} - {reqState === ElasticRequestState.Loading && ( - - {' '} - - - )} + {reqState === ElasticRequestState.Loading && ( + + {' '} + + + )} - {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( -
- -
- )} -
+ {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( +
+ +
+ )} + + ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx index 1aad7e953b8de0..f8fc966dec3519 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx @@ -17,9 +17,9 @@ * under the License. */ import React, { Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import { getAngularModule, wrapInI18nContext, getServices } from '../../../kibana_services'; +import { getAngularModule, getServices } from '../../../kibana_services'; interface Props { fetchError: { @@ -72,26 +72,28 @@ const DiscoverFetchError = ({ fetchError }: Props) => { } return ( - - + + + - - - - {body} + + + + {body} - {fetchError.error} - - - + {fetchError.error} + + + - - + + + ); }; export function createFetchErrorDirective(reactDirective: any) { - return reactDirective(wrapInI18nContext(DiscoverFetchError)); + return reactDirective(DiscoverFetchError); } getAngularModule().directive('discoverFetchError', createFetchErrorDirective); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx index 5984df9c76e61f..0adda0e484843a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx @@ -20,7 +20,7 @@ import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiTitle } from '@elastic/eui'; import { sortBy } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; @@ -162,165 +162,175 @@ export function DiscoverSidebar({ } return ( -
- o.attributes.title)} - /> -
-
- - -
-
- {fields.length > 0 && ( - <> - -

- -

-
-
    - {selectedFields.map((field: IndexPatternField, idx: number) => { - return ( -
  • - -
  • - ); - })} -
-
- + +
+ o.attributes.title)} + /> +
+
+ + +
+
+ {fields.length > 0 && ( + <> +

-
- setShowFields(!showFields)} - aria-label={ - showFields - ? i18n.translate( - 'kbn.discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', - { - defaultMessage: 'Hide fields', - } - ) - : i18n.translate( - 'kbn.discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', - { - defaultMessage: 'Show fields', - } - ) - } - /> +
    + {selectedFields.map((field: IndexPatternField, idx: number) => { + return ( +
  • + +
  • + ); + })} +
+
+ +

+ +

+
+
+ setShowFields(!showFields)} + aria-label={ + showFields + ? i18n.translate( + 'kbn.discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', + { + defaultMessage: 'Hide fields', + } + ) + : i18n.translate( + 'kbn.discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', + { + defaultMessage: 'Show fields', + } + ) + } + /> +
+ + )} + {popularFields.length > 0 && ( +
+ + + +
    + {popularFields.map((field: IndexPatternField, idx: number) => { + return ( +
  • + +
  • + ); + })} +
- - )} - {popularFields.length > 0 && ( -
- - - -
    - {popularFields.map((field: IndexPatternField, idx: number) => { - return ( -
  • - -
  • - ); - })} -
-
- )} + )} -
    - {unpopularFields.map((field: IndexPatternField, idx: number) => { - return ( -
  • - -
  • - ); - })} -
-
-
+
    + {unpopularFields.map((field: IndexPatternField, idx: number) => { + return ( +
  • + +
  • + ); + })} +
+
+
+ ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar_directive.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar_directive.ts index 9dcb459f83613d..624ec0f7578947 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar_directive.ts @@ -16,11 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { wrapInI18nContext } from '../../../kibana_services'; import { DiscoverSidebar } from './discover_sidebar'; export function createDiscoverSidebarDirective(reactDirective: any) { - return reactDirective(wrapInI18nContext(DiscoverSidebar), [ + return reactDirective(DiscoverSidebar, [ ['columns', { watchDepth: 'reference' }], ['fieldCounts', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], 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 bc3737524e1259..a4fffc6eec26da 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/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 25bd36829b6d03..40471b95d774c1 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 bbb6bf26e5b311..648bf7f8f97388 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 e0c43105cb320f..b23b1e3ad9051e 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 80582cc1fbd921..a58bf10c9dab8b 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 50c5a58d35db3b..47cb773258cb46 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 5a8460fcb51bae..40583af7177fee 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 3848c425e2d495..3b1b7a3b52a5b8 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 6d302ac5a74f35..594430ca01f4ce 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 310797a7f3a0cc..a8376c0e84bf9b 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 a5e34f8955fe30..7b9c17640a0f3e 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 97c0d5b89d657b..5d14c4609b918c 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(), })); diff --git a/src/legacy/core_plugins/kibana/public/visualize/_index.scss b/src/legacy/core_plugins/kibana/public/visualize/_index.scss index 0632831578bd01..079d82936bb574 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/_index.scss +++ b/src/legacy/core_plugins/kibana/public/visualize/_index.scss @@ -1,2 +1,5 @@ // Visualize plugin styles @import 'np_ready/index'; + +// should be removed while moving the visualize into NP +@import '../../../../../plugins/vis_default_editor/public/index' diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index addc608efd57d8..d5440c4677d8ae 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -36,7 +36,7 @@ import { VisualizationsStart } from '../../../../../plugins/visualizations/publi import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; -import { DefaultEditorController } from '../../../vis_default_editor/public'; +import { DefaultEditorController } from '../../../../../plugins/vis_default_editor/public'; export interface VisualizeKibanaServices { pluginInitializerContext: PluginInitializerContext; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 81d8458010f196..4ffbc307c69a83 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -51,7 +51,7 @@ import { HomePublicPluginSetup, } from '../../../../../plugins/home/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { DefaultEditorController } from '../../../vis_default_editor/public'; +import { DefaultEditorController } from '../../../../../plugins/vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; diff --git a/src/legacy/core_plugins/management/index.ts b/src/legacy/core_plugins/management/index.ts deleted file mode 100644 index 4962c948f842f2..00000000000000 --- 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 77d33a7bce3b6c..00000000000000 --- 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 96d2c74398a0e8..00000000000000 --- 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/region_map/public/components/region_map_options.tsx b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx index 4219e2e7151508..61cfbf00ded9e6 100644 --- a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx +++ b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { FileLayerField, VectorLayer, ServiceSettings } from 'ui/vis/map/service_settings'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { NumberInputOption, SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { WmsOptions } from '../../../tile_map/public/components/wms_options'; import { RegionMapVisParams } from '../types'; diff --git a/src/legacy/core_plugins/region_map/public/region_map_type.js b/src/legacy/core_plugins/region_map/public/region_map_type.js index 4faa3f92abb5a6..9174b03cf843cf 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_type.js +++ b/src/legacy/core_plugins/region_map/public/region_map_type.js @@ -22,7 +22,7 @@ import { mapToLayerWithId } from './util'; import { createRegionMapVisualization } from './region_map_visualization'; import { RegionMapOptions } from './components/region_map_options'; import { truncatedColorSchemas } from '../../../../plugins/charts/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; // TODO: reference to TILE_MAP plugin should be removed import { ORIGIN } from '../../tile_map/common/origin'; diff --git a/src/legacy/core_plugins/region_map/public/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/region_map_visualization.js index 25641ea76809df..72f9d66e7d2bf4 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/region_map_visualization.js @@ -164,7 +164,8 @@ export function createRegionMapVisualization({ serviceSettings, $injector, uiSet } this._choroplethLayer.on('select', event => { - const rowIndex = this._chartData.rows.findIndex(row => row[0] === event); + const { rows, columns } = this._chartData; + const rowIndex = rows.findIndex(row => row[columns[0].id] === event); this._vis.API.events.filter({ table: this._chartData, column: 0, diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx index 168f56b771b7e5..9169647aa2aef2 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx @@ -21,7 +21,7 @@ import React, { useEffect } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { BasicOptions, RangeOption, diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index 39d39a4c8f8fcd..fe82ad5c7352b2 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; import { MapTypes } from './map_types'; diff --git a/src/legacy/core_plugins/vis_default_editor/index.ts b/src/legacy/core_plugins/vis_default_editor/index.ts deleted file mode 100644 index ee7b5ee4a62ffb..00000000000000 --- a/src/legacy/core_plugins/vis_default_editor/index.ts +++ /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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const vidDefaultEditorPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'vis_default_editor', - require: [], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default vidDefaultEditorPluginInitializer; diff --git a/src/legacy/core_plugins/vis_default_editor/package.json b/src/legacy/core_plugins/vis_default_editor/package.json deleted file mode 100644 index 77dcaff41da6bc..00000000000000 --- a/src/legacy/core_plugins/vis_default_editor/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "vis_default_editor", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx b/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx index 8a4297d3b8149c..a6349793619a01 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { MarkdownVisParams } from './types'; function MarkdownOptions({ stateParams, setValue }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts index b84d9638eb973d..57ea6d9c9bb3d6 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; import { SettingsOptions } from './settings_options'; -import { DefaultEditorSize } from '../../vis_default_editor/public'; +import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; export const markdownVisDefinition = { name: 'markdown', diff --git a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx b/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx index ac1d4bcc82ceca..552fd633735543 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { RangeOption, SwitchOption } from '../../vis_type_vislib/public'; import { MarkdownVisParams } from './types'; diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx index 661f16d6497ba3..5c3032511f09af 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -29,7 +29,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { ColorModes, ColorRanges, diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts index 22c32895d68035..4094cd4eff0601 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts @@ -23,10 +23,6 @@ import { functionWrapper } from '../../../../plugins/expressions/common/expressi jest.mock('ui/new_platform'); -jest.mock('../../vis_default_editor/public', () => ({ - Schemas: class {}, -})); - describe('interpreter/functions#metric', () => { const fn = functionWrapper(createMetricVisFn()); const context = { diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts index 459da47556307c..706693eff10072 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts @@ -22,10 +22,6 @@ import { MetricVisComponent } from './components/metric_vis_component'; jest.mock('ui/new_platform'); -jest.mock('../../vis_default_editor/public', () => ({ - Schemas: class {}, -})); - describe('metric_vis - createMetricVisTypeDefinition', () => { it('has metric vis component set', () => { const def = createMetricVisTypeDefinition(); diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts index f29164f7e540d1..3bbb8964122e54 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts @@ -24,7 +24,7 @@ import { MetricVisOptions } from './components/metric_vis_options'; import { ColorModes } from '../../vis_type_vislib/public'; import { ColorSchemas, colorSchemas } from '../../../../plugins/charts/public'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; export const createMetricVisTypeDefinition = () => ({ name: 'metric', 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 5af11bc7f0b039..b303ccd5aeed20 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/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index 30a95262731668..d01ab31e0a843b 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -23,7 +23,7 @@ import { EuiIconTip, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { search } from '../../../../../plugins/data/public'; import { NumberInputOption, SwitchOption, SelectOption } from '../../../vis_type_vislib/public'; import { TableVisParams } from '../types'; 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 08efed733cafe5..b4b491ac7a5552 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_table/public/table_vis_type.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts index d26e860e51272a..43816121bc23bc 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Vis } from '../../../../plugins/visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index a9e816f70cf531..80e4e1de7ddab6 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,8 +20,8 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; -import { VisOptionsProps } from '../../../vis_default_editor/public'; import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { TagCloudVisParams } from '../types'; 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 fef46282eb8ddd..272bed3e91a08c 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_tagcloud/public/tag_cloud_type.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 5a8cc3004a3154..b7dfa62c93fb97 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { TagCloudOptions } from './components/tag_cloud_options'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx index 6e29b111d422ac..8a8e1b22fb78db 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx +++ b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { search } from '../../../../../plugins/data/public'; const { isValidEsInterval } = search.aggs; -import { useValidation } from '../../../vis_default_editor/public'; +import { useValidation } from '../../../../../plugins/vis_default_editor/public'; const intervalOptions = [ { diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx b/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx index b7c40e15c11fd4..afffcf7ccaf7a7 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx +++ b/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx @@ -20,9 +20,9 @@ import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { VisParams } from './timelion_vis_fn'; import { TimelionInterval, TimelionExpressionInput } from './components'; -import { VisOptionsProps } from '../../vis_default_editor/public'; function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps) { const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [ diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx index 6679553004097b..5be77b3e51a6ad 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { KibanaContextProvider } from '../../../../plugins/kibana_react/public'; -import { DefaultEditorSize } from '../../vis_default_editor/public'; +import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; import { TimelionVisComponent, TimelionVisComponentProp } from './components'; import { TimelionOptions } from './timelion_options'; 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 30c62d778933bf..1db35c406eb132 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 64a9aaaf3b7a68..b2f3e5b2241e61 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/components/vega_vis_editor.tsx b/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx index 707a6830b5ba45..31144065d7b40b 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx @@ -24,11 +24,11 @@ import compactStringify from 'json-stringify-pretty-compact'; import hjson from 'hjson'; import { i18n } from '@kbn/i18n'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; import { VegaActionsMenu } from './vega_actions_menu'; -import { VisOptionsProps } from '../../../vis_default_editor/public'; const aceOptions = { maxLines: Infinity, 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 b0ec90d2c378f1..f56d7682efc6f9 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 @@ -18,11 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { DefaultEditorSize } from '../../vis_default_editor/public'; +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/area.ts b/src/legacy/core_plugins/vis_type_vislib/public/area.ts index e79555470298b8..68decacaaa0404 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/area.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/area.ts @@ -24,7 +24,7 @@ import { palettes } from '@elastic/eui/lib/services'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx index 1138f66d21cfa5..baf3e8ecd1b289 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { SwitchOption } from './switch'; import { SelectOption } from './select'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx index 2c9b1b543e8c23..84c70f10b12da7 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx @@ -22,7 +22,10 @@ import { last } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { RangeValues, RangesParamEditor } from '../../../../vis_default_editor/public'; +import { + RangeValues, + RangesParamEditor, +} from '../../../../../../plugins/vis_default_editor/public'; export type SetColorRangeValue = (paramName: string, value: RangeValues[]) => void; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_schema.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_schema.tsx index 06ce0a2b4af64b..35d7f7b13235e4 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_schema.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_schema.tsx @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { SelectOption } from './select'; import { SwitchOption } from './switch'; import { ColorSchemaVislibParams } from '../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx index c069d4c935669e..718056fd85492b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useState, useCallback } from 'react'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; export interface ValidationVisOptionsProps extends VisOptionsProps { setMultipleValidity(paramName: string, isValid: boolean): void; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx index 706035a7b814ed..6109b548f9412c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx @@ -20,7 +20,7 @@ import React, { useCallback } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { GaugeVisParams } from '../../../gauge'; import { RangesPanel } from './ranges_panel'; import { StylePanel } from './style_panel'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx index 452b9ed9bdbb16..715b5902b69da2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx @@ -23,7 +23,7 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { BasicOptions, ColorRanges, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx index c74f0ef765c8dc..38811bd8364595 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx @@ -23,7 +23,7 @@ import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { ValueAxis } from '../../../types'; import { HeatmapVisParams } from '../../../heatmap'; import { SwitchOption } from '../../common'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap index 442bc826d51fcc..09e0753d592e52 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -54,6 +54,7 @@ exports[`MetricsAxisOptions component should init with the default set of props } vis={ Object { + "serialize": [MockFunction], "setState": [MockFunction], "type": Object { "schemas": Object { @@ -126,6 +127,7 @@ exports[`MetricsAxisOptions component should init with the default set of props } vis={ Object { + "serialize": [MockFunction], "setState": [MockFunction], "type": Object { "schemas": Object { @@ -169,6 +171,7 @@ exports[`MetricsAxisOptions component should init with the default set of props setCategoryAxis={[Function]} vis={ Object { + "serialize": [MockFunction], "setState": [MockFunction], "type": Object { "schemas": Object { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx index 049df0cdd77be9..915885388640c2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx @@ -23,7 +23,7 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { Axis } from '../../../types'; import { SelectOption, SwitchOption } from '../../common'; import { LabelOptions, SetAxisLabel } from './label_options'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx index a3f150e7188175..524792d1460fe3 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx @@ -95,6 +95,7 @@ describe('MetricsAxisOptions component', () => { schemas: { metrics: [{ name: 'metric' }] }, }, setState: jest.fn(), + serialize: jest.fn(), }, stateParams: { valueAxes: [axis], diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx index c7b4562b1087e4..114305d653dd1c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx @@ -299,7 +299,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) }, [stateParams.seriesParams]); useEffect(() => { - vis.setState({ type: visType } as any); + vis.setState({ ...vis.serialize(), type: visType }); }, [vis, visType]); return isTabSelected ? ( diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx index 2182edafb3ebf2..4c0be456aad64b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx @@ -22,7 +22,7 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { BasicOptions, TruncateLabelsOption, SwitchOption } from '../common'; import { PieVisParams } from '../../pie'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx index db9acafac305c6..bb2b3f8fddb492 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx @@ -22,7 +22,7 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { SelectOption, SwitchOption } from '../../common'; import { BasicVislibParams, ValueAxis } from '../../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts b/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts index 4610bd37db5f1a..7ad821dbf2f30a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { RangeValues, Schemas } from '../../../../plugins/vis_default_editor/public'; import { AggGroupNames } from '../../../../plugins/data/public'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, Alignments, ColorModes, GaugeTypes } from './utils/collections'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts b/src/legacy/core_plugins/vis_type_vislib/public/goal.ts index c918128d01f110..6c311bebe07172 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/goal.ts @@ -25,7 +25,7 @@ import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { ColorSchemas } from '../../../../plugins/charts/public'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'goal', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts b/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts index 39a583f3c96411..88b4f0fcaf87e0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { RangeValues, Schemas } from '../../../../plugins/vis_default_editor/public'; import { AggGroupNames } from '../../../../plugins/data/public'; import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; import { HeatmapOptions } from './components/options'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts b/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts index 15ef369e5150e2..54ec8f98203e25 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts @@ -24,7 +24,7 @@ import { palettes } from '@elastic/eui/lib/services'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts index 8b5811628855c8..dc47252ccd44fe 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts @@ -24,7 +24,7 @@ import { palettes } from '@elastic/eui/lib/services'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/line.ts b/src/legacy/core_plugins/vis_type_vislib/public/line.ts index ac4cda869fe295..885ab295d11e1f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/line.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/line.ts @@ -24,7 +24,7 @@ import { palettes } from '@elastic/eui/lib/services'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts b/src/legacy/core_plugins/vis_type_vislib/public/pie.ts index 0f1bd93f5b5bd5..2774836baa3817 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/pie.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { PieOptions } from './components/options'; import { getPositions, Positions } from './utils/collections'; import { createVislibVisController } from './vis_controller'; 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 da50e227d84d20..0d6b1b5e8de589 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/utils/common_config.tsx b/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx index 6da40686a8b506..de867dc72bba7a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from '../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { PointSeriesOptions, MetricsAxisOptions } from '../components/options'; import { ValidationWrapper } from '../components/common'; import { BasicVislibParams } from '../types'; diff --git a/src/legacy/server/status/collectors/get_ops_stats_collector.js b/src/legacy/server/status/collectors/get_ops_stats_collector.js deleted file mode 100644 index b733e2e721e3a5..00000000000000 --- a/src/legacy/server/status/collectors/get_ops_stats_collector.js +++ /dev/null @@ -1,56 +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 { KIBANA_STATS_TYPE } from '../constants'; -import { getKibanaInfoForStats } from '../lib'; - -/* - * Initialize a collector for Kibana Ops Stats - * - * NOTE this collector's fetch method returns the latest stats from the - * Hapi/Good/Even-Better ops event listener. Therefore, the stats reset - * every 5 seconds (the default value of the ops.interval configuration - * setting). That makes it geared for providing the latest "real-time" - * stats. In the long-term, fetch should return stats that constantly - * accumulate over the server's uptime for better machine readability. - * Since the data is captured, timestamped and stored, the historical - * data can provide "real-time" stats by calculating a derivative of - * the metrics. - * See PR comment in https://github.com/elastic/kibana/pull/20577/files#r202416647 - */ -export function getOpsStatsCollector(usageCollection, server, kbnServer) { - return usageCollection.makeStatsCollector({ - type: KIBANA_STATS_TYPE, - fetch: () => { - return { - kibana: getKibanaInfoForStats(server, kbnServer), - ...kbnServer.metrics, // latest metrics captured from the ops event listener in src/legacy/server/status/index - }; - }, - isReady: () => true, - ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there. - }); -} - -export function registerOpsStatsCollector(usageCollection, server, kbnServer) { - if (usageCollection) { - const collector = getOpsStatsCollector(usageCollection, server, kbnServer); - usageCollection.registerCollector(collector); - } -} diff --git a/src/legacy/server/status/index.js b/src/legacy/server/status/index.js index df02b3c45ec2f8..5bd1efa99eb2c6 100644 --- a/src/legacy/server/status/index.js +++ b/src/legacy/server/status/index.js @@ -20,7 +20,6 @@ import ServerStatus from './server_status'; import { Metrics } from './lib/metrics'; import { registerStatusPage, registerStatusApi, registerStatsApi } from './routes'; -import { registerOpsStatsCollector } from './collectors'; import Oppsy from 'oppsy'; import { cloneDeep } from 'lodash'; import { getOSInfo } from './lib/get_os_info'; @@ -28,7 +27,6 @@ import { getOSInfo } from './lib/get_os_info'; export function statusMixin(kbnServer, server, config) { kbnServer.status = new ServerStatus(kbnServer.server); const { usageCollection } = server.newPlatform.setup.plugins; - registerOpsStatsCollector(usageCollection, server, kbnServer); const metrics = new Metrics(config, server); 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 f70ef069dd134e..0779d6472671cb 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 b4b5099081759e..cdd7e1a9949127 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/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index ad4aa97d8ea7a0..1093153edbbf7e 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/discover/public/services.ts b/src/plugins/discover/public/services.ts index 3a28759d82b71d..37e2144800ea11 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 56facc37fc6661..ddd84b05443455 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/embeddable/public/lib/embeddables/error_embeddable.test.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx index 816001ba42ff1d..715827a72c61b9 100644 --- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx @@ -20,8 +20,6 @@ import React from 'react'; import { ErrorEmbeddable } from './error_embeddable'; import { EmbeddableRoot } from './embeddable_root'; import { mount } from 'enzyme'; -// @ts-ignore -import { findTestSubject } from '@elastic/eui/lib/test'; test('ErrorEmbeddable renders an embeddable', async () => { const embeddable = new ErrorEmbeddable('some error occurred', { id: '123', title: 'Error' }); diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json new file mode 100644 index 00000000000000..5a3db3b3440901 --- /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 944b800c66a284..6db6248f4c68fa 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/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json new file mode 100644 index 00000000000000..d5397a11184aab --- /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 bae0f1d3e23cdb..da482c0c51f0ac 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 ae0be98de63f37..bc97f46c302e32 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 2a8ef10c817cc9..93bb0ead1df4af 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 5714fa33389624..29ab0ebfc3d5f7 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 e7fa13409ab046..32b3e7ee7a133b 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 2b6f008dd928a1..4780fa00ed468d 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 dd4d77a6811717..87c246e8913e53 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 73ca33ae914a9b..3a2910a222cd7c 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 00000000000000..0add1bee84ae02 --- /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 b4007b30cf8cab..21bba92ada4c16 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 939d372b9997f7..aefbd66e50fcf4 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 9b34880cf4fe3a..ee99ea36726727 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 e1689e38dbfe05..9ad9f14ac5659d 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 00000000000000..6fa39d82d10215 --- /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 1876e688c989ac..2f139050e994ab 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/telemetry/common/constants.ts b/src/plugins/telemetry/common/constants.ts index babd009143c5ef..fd32862896528e 100644 --- a/src/plugins/telemetry/common/constants.ts +++ b/src/plugins/telemetry/common/constants.ts @@ -80,3 +80,14 @@ export const APPLICATION_USAGE_TYPE = 'application_usage'; * The type name used within the Monitoring index to publish management stats. */ export const KIBANA_STACK_MANAGEMENT_STATS_TYPE = 'stack_management'; + +/** + * The type name used to publish Kibana usage stats. + * NOTE: this string shows as-is in the stats API as a field name for the kibana usage stats + */ +export const KIBANA_USAGE_TYPE = 'kibana'; + +/** + * The type name used to publish Kibana usage stats in the formatted as bulk. + */ +export const KIBANA_STATS_TYPE = 'kibana_stats'; diff --git a/src/plugins/telemetry/server/collectors/index.ts b/src/plugins/telemetry/server/collectors/index.ts index 6eeda080bb3ab9..a874f8dd6202cc 100644 --- a/src/plugins/telemetry/server/collectors/index.ts +++ b/src/plugins/telemetry/server/collectors/index.ts @@ -22,3 +22,5 @@ export { registerUiMetricUsageCollector } from './ui_metric'; export { registerTelemetryPluginUsageCollector } from './telemetry_plugin'; export { registerManagementUsageCollector } from './management'; export { registerApplicationUsageCollector } from './application_usage'; +export { registerKibanaUsageCollector } from './kibana'; +export { registerOpsStatsCollector } from './ops_stats'; diff --git a/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.test.ts b/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.test.ts new file mode 100644 index 00000000000000..a7681e17664272 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { getSavedObjectsCounts } from './get_saved_object_counts'; + +describe('getSavedObjectsCounts', () => { + test('Get all the saved objects equal to 0 because no results were found', async () => { + const callCluster = jest.fn(() => ({})); + + const results = await getSavedObjectsCounts(callCluster as any, '.kibana'); + expect(results).toStrictEqual({ + dashboard: { total: 0 }, + visualization: { total: 0 }, + search: { total: 0 }, + index_pattern: { total: 0 }, + graph_workspace: { total: 0 }, + timelion_sheet: { total: 0 }, + }); + }); + + test('Merge the zeros with the results', async () => { + const callCluster = jest.fn(() => ({ + aggregations: { + types: { + buckets: [ + { key: 'dashboard', doc_count: 1 }, + { key: 'timelion-sheet', doc_count: 2 }, + { key: 'index-pattern', value: 2 }, // Malformed on purpose + { key: 'graph_workspace', doc_count: 3 }, // already snake_cased + ], + }, + }, + })); + + const results = await getSavedObjectsCounts(callCluster as any, '.kibana'); + expect(results).toStrictEqual({ + dashboard: { total: 1 }, + visualization: { total: 0 }, + search: { total: 0 }, + index_pattern: { total: 0 }, + graph_workspace: { total: 3 }, + timelion_sheet: { total: 2 }, + }); + }); +}); diff --git a/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.ts b/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.ts new file mode 100644 index 00000000000000..804c8b0ed2026e --- /dev/null +++ b/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.ts @@ -0,0 +1,82 @@ +/* + * 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. + */ + +/** + * Moved from /x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts + * + * The PR https://github.com/elastic/kibana/pull/62665 proved what the issue https://github.com/elastic/kibana/issues/58249 + * was claiming: the structure and payload for common telemetry bits differs between Monitoring and OSS/X-Pack collections. + * + * Unifying this logic from Monitoring that makes sense to have in OSS here and we will import it on the monitoring side to reuse it. + */ + +import { snakeCase } from 'lodash'; +import { APICaller } from 'kibana/server'; + +const TYPES = [ + 'dashboard', + 'visualization', + 'search', + 'index-pattern', + 'graph-workspace', + 'timelion-sheet', +]; + +export interface KibanaSavedObjectCounts { + [pluginName: string]: { + total: number; + }; +} + +export async function getSavedObjectsCounts( + callCluster: APICaller, + kibanaIndex: string // Typically '.kibana'. We might need a way to obtain it from the SavedObjects client (or the SavedObjects client to provide a way to run aggregations?) +): Promise { + const savedObjectCountSearchParams = { + index: kibanaIndex, + ignoreUnavailable: true, + filterPath: 'aggregations.types.buckets', + body: { + size: 0, + query: { + terms: { type: TYPES }, + }, + aggs: { + types: { + terms: { field: 'type', size: TYPES.length }, + }, + }, + }, + }; + const resp = await callCluster('search', savedObjectCountSearchParams); + const buckets: Array<{ key: string; doc_count: number }> = + resp.aggregations?.types?.buckets || []; + + // Initialise the object with all zeros for all the types + const allZeros: KibanaSavedObjectCounts = TYPES.reduce( + (acc, type) => ({ ...acc, [snakeCase(type)]: { total: 0 } }), + {} + ); + + // Add the doc_count from each bucket + return buckets.reduce( + (acc, { key, doc_count: total }) => (total ? { ...acc, [snakeCase(key)]: { total } } : acc), + allZeros + ); +} diff --git a/src/plugins/telemetry/server/collectors/kibana/index.test.ts b/src/plugins/telemetry/server/collectors/kibana/index.test.ts new file mode 100644 index 00000000000000..91ede686ded3da --- /dev/null +++ b/src/plugins/telemetry/server/collectors/kibana/index.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { UsageCollectionSetup } from '../../../../../plugins/usage_collection/server'; +import { pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CollectorOptions } from '../../../../../plugins/usage_collection/server/collector/collector'; + +import { registerKibanaUsageCollector } from './'; + +describe('telemetry_kibana', () => { + let collector: CollectorOptions; + + const usageCollectionMock: jest.Mocked = { + makeUsageCollector: jest.fn().mockImplementation(config => (collector = config)), + registerCollector: jest.fn(), + } as any; + + const legacyConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; + const callCluster = jest.fn().mockImplementation(() => ({})); + + beforeAll(() => registerKibanaUsageCollector(usageCollectionMock, legacyConfig$)); + afterAll(() => jest.clearAllTimers()); + + test('registered collector is set', () => { + expect(collector).not.toBeUndefined(); + expect(collector.type).toBe('kibana'); + }); + + test('fetch', async () => { + expect(await collector.fetch(callCluster)).toStrictEqual({ + index: '.kibana-tests', + dashboard: { total: 0 }, + visualization: { total: 0 }, + search: { total: 0 }, + index_pattern: { total: 0 }, + graph_workspace: { total: 0 }, + timelion_sheet: { total: 0 }, + }); + }); + + test('formatForBulkUpload', async () => { + const resultFromFetch = { + index: '.kibana-tests', + dashboard: { total: 0 }, + visualization: { total: 0 }, + search: { total: 0 }, + index_pattern: { total: 0 }, + graph_workspace: { total: 0 }, + timelion_sheet: { total: 0 }, + }; + + expect(collector.formatForBulkUpload!(resultFromFetch)).toStrictEqual({ + type: 'kibana_stats', + payload: { + usage: resultFromFetch, + }, + }); + }); +}); diff --git a/src/legacy/server/status/collectors/index.js b/src/plugins/telemetry/server/collectors/kibana/index.ts similarity index 91% rename from src/legacy/server/status/collectors/index.js rename to src/plugins/telemetry/server/collectors/kibana/index.ts index 92d9e601bbb35e..695d972346b8a5 100644 --- a/src/legacy/server/status/collectors/index.js +++ b/src/plugins/telemetry/server/collectors/kibana/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { registerOpsStatsCollector } from './get_ops_stats_collector'; +export { registerKibanaUsageCollector } from './kibana_usage_collector'; diff --git a/src/plugins/telemetry/server/collectors/kibana/kibana_usage_collector.ts b/src/plugins/telemetry/server/collectors/kibana/kibana_usage_collector.ts new file mode 100644 index 00000000000000..ccf6f7b1033c94 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/kibana/kibana_usage_collector.ts @@ -0,0 +1,65 @@ +/* + * 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 { take } from 'rxjs/operators'; +import { SharedGlobalConfig } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { KIBANA_STATS_TYPE, KIBANA_USAGE_TYPE } from '../../../common/constants'; +import { getSavedObjectsCounts } from './get_saved_object_counts'; + +export function getKibanaUsageCollector( + usageCollection: UsageCollectionSetup, + legacyConfig$: Observable +) { + return usageCollection.makeUsageCollector({ + type: KIBANA_USAGE_TYPE, + isReady: () => true, + async fetch(callCluster) { + const { + kibana: { index }, + } = await legacyConfig$.pipe(take(1)).toPromise(); + return { + index, + ...(await getSavedObjectsCounts(callCluster, index)), + }; + }, + + /* + * Format the response data into a model for internal upload + * 1. Make this data part of the "kibana_stats" type + * 2. Organize the payload in the usage namespace of the data payload (usage.index, etc) + */ + formatForBulkUpload: result => { + return { + type: KIBANA_STATS_TYPE, + payload: { + usage: result, + }, + }; + }, + }); +} + +export function registerKibanaUsageCollector( + usageCollection: UsageCollectionSetup, + legacyConfig$: Observable +) { + usageCollection.registerCollector(getKibanaUsageCollector(usageCollection, legacyConfig$)); +} diff --git a/src/plugins/telemetry/server/collectors/ops_stats/__snapshots__/index.test.ts.snap b/src/plugins/telemetry/server/collectors/ops_stats/__snapshots__/index.test.ts.snap new file mode 100644 index 00000000000000..678237ffb6ea23 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/ops_stats/__snapshots__/index.test.ts.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`telemetry_ops_stats should return something when there is a metric 1`] = ` +Object { + "concurrent_connections": 20, + "os": Object { + "load": Object { + "15m": 3, + "1m": 0.5, + "5m": 1, + }, + "memory": Object { + "free_in_bytes": 10, + "total_in_bytes": 10, + "used_in_bytes": 10, + }, + "platform": "darwin", + "platformRelease": "test", + "uptime_in_millis": 1000, + }, + "process": Object { + "event_loop_delay": 10, + "memory": Object { + "heap": Object { + "size_limit": 0, + "total_in_bytes": 0, + "used_in_bytes": 0, + }, + "resident_set_size_in_bytes": 0, + }, + "uptime_in_millis": 1000, + }, + "requests": Object { + "disconnects": 10, + "total": 100, + }, + "response_times": Object { + "average": 100, + "max": 200, + }, + "timestamp": Any, +} +`; diff --git a/src/plugins/telemetry/server/collectors/ops_stats/index.test.ts b/src/plugins/telemetry/server/collectors/ops_stats/index.test.ts new file mode 100644 index 00000000000000..92e0e40776eb89 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/ops_stats/index.test.ts @@ -0,0 +1,132 @@ +/* + * 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 { Subject } from 'rxjs'; +import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CollectorOptions } from '../../../../../plugins/usage_collection/server/collector/collector'; + +import { registerOpsStatsCollector } from './'; +import { OpsMetrics } from '../../../../../core/server'; + +describe('telemetry_ops_stats', () => { + let collector: CollectorOptions; + + const usageCollectionMock: jest.Mocked = { + makeStatsCollector: jest.fn().mockImplementation(config => (collector = config)), + registerCollector: jest.fn(), + } as any; + + const metrics$ = new Subject(); + const callCluster = jest.fn(); + + const metric: OpsMetrics = { + process: { + memory: { + heap: { + total_in_bytes: 0, + used_in_bytes: 0, + size_limit: 0, + }, + resident_set_size_in_bytes: 0, + }, + event_loop_delay: 10, + pid: 10, + uptime_in_millis: 1000, + }, + os: { + platform: 'darwin', + platformRelease: 'test', + load: { + '1m': 0.5, + '5m': 1, + '15m': 3, + }, + memory: { + total_in_bytes: 10, + free_in_bytes: 10, + used_in_bytes: 10, + }, + uptime_in_millis: 1000, + }, + response_times: { avg_in_millis: 100, max_in_millis: 200 }, + requests: { + disconnects: 10, + total: 100, + statusCodes: { 200: 100 }, + }, + concurrent_connections: 20, + }; + + beforeAll(() => registerOpsStatsCollector(usageCollectionMock, metrics$)); + afterAll(() => jest.clearAllTimers()); + + test('registered collector is set', () => { + expect(collector).not.toBeUndefined(); + expect(collector.type).toBe('kibana_stats'); + }); + + test('isReady should return false because no metrics have been provided yet', () => { + expect(collector.isReady()).toBe(false); + }); + + test('should return something when there is a metric', async () => { + metrics$.next(metric); + expect(collector.isReady()).toBe(true); + expect(await collector.fetch(callCluster)).toMatchSnapshot({ + concurrent_connections: 20, + os: { + load: { + '15m': 3, + '1m': 0.5, + '5m': 1, + }, + memory: { + free_in_bytes: 10, + total_in_bytes: 10, + used_in_bytes: 10, + }, + platform: 'darwin', + platformRelease: 'test', + uptime_in_millis: 1000, + }, + process: { + event_loop_delay: 10, + memory: { + heap: { + size_limit: 0, + total_in_bytes: 0, + used_in_bytes: 0, + }, + resident_set_size_in_bytes: 0, + }, + uptime_in_millis: 1000, + }, + requests: { + disconnects: 10, + total: 100, + }, + response_times: { + average: 100, + max: 200, + }, + timestamp: expect.any(String), + }); + }); +}); diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index.ts b/src/plugins/telemetry/server/collectors/ops_stats/index.ts similarity index 92% rename from src/legacy/core_plugins/management/public/np_ready/services/index.ts rename to src/plugins/telemetry/server/collectors/ops_stats/index.ts index 9df010223542be..443a25749d2003 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index.ts +++ b/src/plugins/telemetry/server/collectors/ops_stats/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './index_pattern_management'; +export { registerOpsStatsCollector } from './ops_stats_collector'; diff --git a/src/plugins/telemetry/server/collectors/ops_stats/ops_stats_collector.ts b/src/plugins/telemetry/server/collectors/ops_stats/ops_stats_collector.ts new file mode 100644 index 00000000000000..4967e20006ddd7 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/ops_stats/ops_stats_collector.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 { Observable } from 'rxjs'; +import { cloneDeep } from 'lodash'; +import moment from 'moment'; +import { OpsMetrics } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { KIBANA_STATS_TYPE } from '../../../common/constants'; + +interface OpsStatsMetrics extends Omit { + timestamp: string; + response_times: { + average: number; + max: number; + }; +} + +/** + * Initialize a collector for Kibana Ops Stats + */ +export function getOpsStatsCollector( + usageCollection: UsageCollectionSetup, + metrics$: Observable +) { + let lastMetrics: OpsStatsMetrics | null = null; + metrics$.subscribe(_metrics => { + const metrics = cloneDeep(_metrics); + // Ensure we only include the same data that Metricbeat collection would get + delete metrics.process.pid; + const responseTimes = { + average: metrics.response_times.avg_in_millis, + max: metrics.response_times.max_in_millis, + }; + delete metrics.requests.statusCodes; + lastMetrics = { + ...metrics, + response_times: responseTimes, + timestamp: moment.utc().toISOString(), + }; + }); + + return usageCollection.makeStatsCollector({ + type: KIBANA_STATS_TYPE, + isReady: () => !!lastMetrics, + fetch: () => lastMetrics, + }); +} + +export function registerOpsStatsCollector( + usageCollection: UsageCollectionSetup, + metrics$: Observable +) { + usageCollection.registerCollector(getOpsStatsCollector(usageCollection, metrics$)); +} diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 77036b4ea7ddc8..1df6a665e4d766 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -32,6 +32,8 @@ import { SavedObjectsClient, Plugin, Logger, + SharedGlobalConfig, + MetricsServiceSetup, } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -41,6 +43,8 @@ import { registerTelemetryPluginUsageCollector, registerManagementUsageCollector, registerApplicationUsageCollector, + registerKibanaUsageCollector, + registerOpsStatsCollector, } from './collectors'; import { TelemetryConfigType } from './config'; import { FetcherTask } from './fetcher'; @@ -61,6 +65,7 @@ export class TelemetryPlugin implements Plugin { private readonly logger: Logger; private readonly currentKibanaVersion: string; private readonly config$: Observable; + private readonly legacyConfig$: Observable; private readonly isDev: boolean; private readonly fetcherTask: FetcherTask; private savedObjectsClient?: ISavedObjectsRepository; @@ -71,6 +76,7 @@ export class TelemetryPlugin implements Plugin { this.isDev = initializerContext.env.mode.dev; this.currentKibanaVersion = initializerContext.env.packageInfo.version; this.config$ = initializerContext.config.create(); + this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; this.fetcherTask = new FetcherTask({ ...initializerContext, logger: this.logger, @@ -78,15 +84,15 @@ export class TelemetryPlugin implements Plugin { } public async setup( - core: CoreSetup, + { elasticsearch, http, savedObjects, metrics }: CoreSetup, { usageCollection, telemetryCollectionManager }: TelemetryPluginsSetup ) { const currentKibanaVersion = this.currentKibanaVersion; const config$ = this.config$; const isDev = this.isDev; - registerCollection(telemetryCollectionManager, core.elasticsearch.dataClient); - const router = core.http.createRouter(); + registerCollection(telemetryCollectionManager, elasticsearch.dataClient); + const router = http.createRouter(); registerRoutes({ config$, @@ -96,8 +102,8 @@ export class TelemetryPlugin implements Plugin { telemetryCollectionManager, }); - this.registerMappings(opts => core.savedObjects.registerType(opts)); - this.registerUsageCollectors(usageCollection, opts => core.savedObjects.registerType(opts)); + this.registerMappings(opts => savedObjects.registerType(opts)); + this.registerUsageCollectors(usageCollection, metrics, opts => savedObjects.registerType(opts)); } public async start(core: CoreStart, { telemetryCollectionManager }: TelemetryPluginsStart) { @@ -153,11 +159,14 @@ export class TelemetryPlugin implements Plugin { private registerUsageCollectors( usageCollection: UsageCollectionSetup, + metrics: MetricsServiceSetup, registerType: SavedObjectsRegisterType ) { const getSavedObjectsClient = () => this.savedObjectsClient; const getUiSettingsClient = () => this.uiSettingsClient; + registerOpsStatsCollector(usageCollection, metrics.getOpsMetrics$()); + registerKibanaUsageCollector(usageCollection, this.legacyConfig$); registerTelemetryPluginUsageCollector(usageCollection, { currentKibanaVersion: this.currentKibanaVersion, config$: this.config$, diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 86c6731e11d37b..a17f1b8232a223 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -55,6 +55,10 @@ export function handleKibanaStats( ...kibanaStats.os, }; const formattedOsStats = Object.entries(os).reduce((acc, [key, value]) => { + if (typeof value !== 'string') { + // There are new fields reported now from the "os" property like "load", "memory", etc. They are objects. + return acc; + } return { ...acc, [`${key}s`]: [{ [key]: value, count: 1 }], diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 2b2fc004a84c62..f532c2c8aa2193 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 c590cf8f34ee07..3eaa13572a826e 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/README.md b/src/plugins/vis_default_editor/README.md new file mode 100644 index 00000000000000..8607dfe486ace3 --- /dev/null +++ b/src/plugins/vis_default_editor/README.md @@ -0,0 +1,16 @@ +# Visualization Deafult Editor plugin + +The default editor is used in most primary visualizations, e.x. `Area`, `Data table`, `Pie`, etc. +It acts as a container for a particular visualization and options tabs. Contains the default "Data" tab in `public/components/sidebar/data_tab.tsx`. +The plugin exposes the static `DefaultEditorController` class to consume. + +```ts +import { DefaultEditorController } from '../../vis_default_editor/public'; + +const editor = new DefaultEditorController( + element, + vis, + eventEmitter, + embeddableHandler +); +``` \ No newline at end of file diff --git a/src/legacy/core_plugins/vis_default_editor/public/_agg.scss b/src/plugins/vis_default_editor/public/_agg.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/_agg.scss rename to src/plugins/vis_default_editor/public/_agg.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/_agg_params.scss b/src/plugins/vis_default_editor/public/_agg_params.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/_agg_params.scss rename to src/plugins/vis_default_editor/public/_agg_params.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/_default.scss rename to src/plugins/vis_default_editor/public/_default.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/_sidebar.scss b/src/plugins/vis_default_editor/public/_sidebar.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/_sidebar.scss rename to src/plugins/vis_default_editor/public/_sidebar.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg.test.tsx b/src/plugins/vis_default_editor/public/components/agg.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg.test.tsx rename to src/plugins/vis_default_editor/public/components/agg.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx b/src/plugins/vis_default_editor/public/components/agg.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx rename to src/plugins/vis_default_editor/public/components/agg.tsx index 83fbf70c9099ef..c7e3e609490f90 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx +++ b/src/plugins/vis_default_editor/public/components/agg.tsx @@ -28,14 +28,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IAggConfig } from 'src/plugins/data/public'; +import { IAggConfig, TimeRange } from 'src/plugins/data/public'; import { DefaultEditorAggParams } from './agg_params'; import { DefaultEditorAggCommonProps } from './agg_common_props'; import { AGGS_ACTION_KEYS, AggsAction } from './agg_group_state'; import { RowsOrColumnsControl } from './controls/rows_or_columns'; import { RadiusRatioOptionControl } from './controls/radius_ratio_option'; import { getSchemaByName } from '../schemas'; -import { TimeRange } from '../../../../../plugins/data/public'; import { buildAggDescription } from './agg_params_helper'; export interface DefaultEditorAggProps extends DefaultEditorAggCommonProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx b/src/plugins/vis_default_editor/public/components/agg_add.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx rename to src/plugins/vis_default_editor/public/components/agg_add.tsx index 9df4ea58e0f075..f2c2f8b4d0b94c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_add.tsx @@ -29,7 +29,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { IAggConfig, AggGroupNames } from '../../../../../plugins/data/public'; +import { IAggConfig, AggGroupNames } from '../../../data/public'; import { Schema } from '../schemas'; interface DefaultEditorAggAddProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_common_props.ts b/src/plugins/vis_default_editor/public/components/agg_common_props.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_common_props.ts rename to src/plugins/vis_default_editor/public/components/agg_common_props.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.test.tsx b/src/plugins/vis_default_editor/public/components/agg_group.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group.test.tsx rename to src/plugins/vis_default_editor/public/components/agg_group.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx rename to src/plugins/vis_default_editor/public/components/agg_group.tsx index 792595fd421f67..ecbc41f28003c9 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggGroupNames, search, IAggConfig } from '../../../../../plugins/data/public'; +import { AggGroupNames, search, IAggConfig, TimeRange } from '../../../data/public'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from './agg_common_props'; @@ -42,7 +42,6 @@ import { } from './agg_group_helper'; import { aggGroupReducer, initAggsState, AGGS_ACTION_KEYS } from './agg_group_state'; import { Schema } from '../schemas'; -import { TimeRange } from '../../../../../plugins/data/public'; export interface DefaultEditorAggGroupProps extends DefaultEditorAggCommonProps { schemas: Schema[]; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_group_helper.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.test.ts rename to src/plugins/vis_default_editor/public/components/agg_group_helper.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.tsx b/src/plugins/vis_default_editor/public/components/agg_group_helper.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.tsx rename to src/plugins/vis_default_editor/public/components/agg_group_helper.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_state.tsx b/src/plugins/vis_default_editor/public/components/agg_group_state.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group_state.tsx rename to src/plugins/vis_default_editor/public/components/agg_group_state.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_param.tsx b/src/plugins/vis_default_editor/public/components/agg_param.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_param.tsx rename to src/plugins/vis_default_editor/public/components/agg_param.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts b/src/plugins/vis_default_editor/public/components/agg_param_props.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts rename to src/plugins/vis_default_editor/public/components/agg_param_props.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx b/src/plugins/vis_default_editor/public/components/agg_params.test.tsx similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx rename to src/plugins/vis_default_editor/public/components/agg_params.test.tsx index 1c49ebf40640ed..cac1b0851b92df 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_params.test.tsx @@ -25,8 +25,8 @@ import { DefaultEditorAggParams as PureDefaultEditorAggParams, DefaultEditorAggParamsProps, } from './agg_params'; -import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; +import { KibanaContextProvider } from '../../../kibana_react/public'; +import { dataPluginMock } from '../../../data/public/mocks'; import { EditorVisState } from './sidebar/state/reducers'; const mockEditorConfig = { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx b/src/plugins/vis_default_editor/public/components/agg_params.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx rename to src/plugins/vis_default_editor/public/components/agg_params.tsx index b1555b76500d0a..3674e39b558d26 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_params.tsx @@ -22,7 +22,7 @@ import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import useUnmount from 'react-use/lib/useUnmount'; -import { IAggConfig, IndexPattern, AggGroupNames } from '../../../../../plugins/data/public'; +import { IAggConfig, IndexPattern, AggGroupNames } from '../../../data/public'; import { DefaultEditorAggSelect } from './agg_select'; import { DefaultEditorAggParam } from './agg_param'; @@ -40,7 +40,7 @@ import { import { DefaultEditorCommonProps } from './agg_common_props'; import { EditorParamConfig, TimeIntervalParam, FixedParam, getEditorConfig } from './utils'; import { Schema, getSchemaByName } from '../schemas'; -import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../kibana_react/public'; import { VisDefaultEditorKibanaServices } from '../types'; const FIXED_VALUE_PROP = 'fixedValue'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts rename to src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts similarity index 99% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts rename to src/plugins/vis_default_editor/public/components/agg_params_helper.ts index 073cb7d5ac66c2..a32bd76bafa5ab 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -34,7 +34,7 @@ import { AggParamEditorProps } from './agg_param_props'; import { aggParamsMap } from './agg_params_map'; import { EditorConfig } from './utils'; import { Schema, getSchemaByName } from '../schemas'; -import { search } from '../../../../../plugins/data/public'; +import { search } from '../../../data/public'; import { EditorVisState } from './sidebar/state/reducers'; interface ParamInstanceBase { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts rename to src/plugins/vis_default_editor/public/components/agg_params_map.ts index 4517313b6fd6e8..5af3cfc5b09289 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -18,12 +18,7 @@ */ import * as controls from './controls'; -import { - AggGroupNames, - BUCKET_TYPES, - METRIC_TYPES, - search, -} from '../../../../../plugins/data/public'; +import { AggGroupNames, BUCKET_TYPES, METRIC_TYPES, search } from '../../../data/public'; import { wrapWithInlineComp } from './controls/utils'; const { siblingPipelineType, parentPipelineType } = search.aggs; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_state.ts b/src/plugins/vis_default_editor/public/components/agg_params_state.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params_state.ts rename to src/plugins/vis_default_editor/public/components/agg_params_state.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx b/src/plugins/vis_default_editor/public/components/agg_select.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx rename to src/plugins/vis_default_editor/public/components/agg_select.tsx index 7ee432946f3c87..6cb76b18e24a6f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_select.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { IAggType, IndexPattern } from 'src/plugins/data/public'; -import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../kibana_react/public'; import { ComboBoxGroupedOptions } from '../utils'; import { AGG_TYPE_ACTION_KEYS, AggTypeAction } from './agg_params_state'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_control_props.tsx b/src/plugins/vis_default_editor/public/components/controls/agg_control_props.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_control_props.tsx rename to src/plugins/vis_default_editor/public/components/controls/agg_control_props.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx b/src/plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/auto_precision.tsx b/src/plugins/vis_default_editor/public/components/controls/auto_precision.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/auto_precision.tsx rename to src/plugins/vis_default_editor/public/components/controls/auto_precision.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx similarity index 68% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx index e52b2c85b63fa2..b874459a8e7d3a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx @@ -17,11 +17,11 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFieldText, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Ipv4Address } from '../../../../../../../plugins/kibana_utils/public'; +import { Ipv4Address } from '../../../../../kibana_utils/public'; import { InputList, InputListConfig, InputModel, InputObject, InputItem } from './input_list'; const EMPTY_STRING = ''; @@ -44,38 +44,42 @@ interface FromToListProps { setValidity(isValid: boolean): void; } -function FromToList({ showValidation, onBlur, ...rest }: FromToListProps) { - const fromToListConfig: InputListConfig = { - defaultValue: { - from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false }, - to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false }, +const defaultConfig = { + defaultValue: { + from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false }, + to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false }, + }, + validateClass: Ipv4Address, + getModelValue: (item: FromToObject = {}) => ({ + from: { + value: item.from || EMPTY_STRING, + model: item.from || EMPTY_STRING, + isInvalid: false, }, - validateClass: Ipv4Address, - getModelValue: (item: FromToObject = {}) => ({ - from: { - value: item.from || EMPTY_STRING, - model: item.from || EMPTY_STRING, - isInvalid: false, - }, - to: { value: item.to || EMPTY_STRING, model: item.to || EMPTY_STRING, isInvalid: false }, + to: { value: item.to || EMPTY_STRING, model: item.to || EMPTY_STRING, isInvalid: false }, + }), + getRemoveBtnAriaLabel: (item: FromToModel) => + i18n.translate('visDefaultEditor.controls.ipRanges.removeRangeAriaLabel', { + defaultMessage: 'Remove the range of {from} to {to}', + values: { from: item.from.value || '*', to: item.to.value || '*' }, }), - getRemoveBtnAriaLabel: (item: FromToModel) => - i18n.translate('visDefaultEditor.controls.ipRanges.removeRangeAriaLabel', { - defaultMessage: 'Remove the range of {from} to {to}', - values: { from: item.from.value || '*', to: item.to.value || '*' }, - }), - onChangeFn: ({ from, to }: FromToModel) => { - const result: FromToObject = {}; - if (from.model) { - result.from = from.model; - } - if (to.model) { - result.to = to.model; - } - return result; - }, - hasInvalidValuesFn: ({ from, to }: FromToModel) => from.isInvalid || to.isInvalid, - renderInputRow: (item: FromToModel, index, onChangeValue) => ( + onChangeFn: ({ from, to }: FromToModel) => { + const result: FromToObject = {}; + if (from.model) { + result.from = from.model; + } + if (to.model) { + result.to = to.model; + } + return result; + }, + hasInvalidValuesFn: ({ from, to }: FromToModel) => from.isInvalid || to.isInvalid, + modelNames: ['from', 'to'], +}; + +function FromToList({ showValidation, onBlur, ...rest }: FromToListProps) { + const renderInputRow = useCallback( + (item: FromToModel, index, onChangeValue) => ( <> ), - modelNames: ['from', 'to'], + [onBlur, showValidation] + ); + const fromToListConfig: InputListConfig = { + ...defaultConfig, + renderInputRow, }; return ; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/input_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx similarity index 75% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/input_list.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx index cc80d0073c9046..639b69cd3d33cb 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/input_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useState, useEffect, Fragment } from 'react'; +import React, { useState, useEffect, Fragment, useCallback } from 'react'; import { isEmpty, isEqual, mapValues, omit, pick } from 'lodash'; import { EuiButtonIcon, @@ -70,7 +70,10 @@ interface InputListProps { } const generateId = htmlIdGenerator(); -const validateValue = (inputValue: string | undefined, config: InputListConfig) => { +const validateValue = ( + inputValue: string | undefined, + InputObject: InputListConfig['validateClass'] +) => { const result = { model: inputValue || '', isInvalid: false, @@ -80,7 +83,6 @@ const validateValue = (inputValue: string | undefined, config: InputListConfig) return result; } try { - const InputObject = config.validateClass; result.model = new InputObject(inputValue).toString(); result.isInvalid = false; return result; @@ -91,47 +93,60 @@ const validateValue = (inputValue: string | undefined, config: InputListConfig) }; function InputList({ config, list, onChange, setValidity }: InputListProps) { + const { defaultValue, getModelValue, modelNames, onChangeFn, validateClass } = config; const [models, setModels] = useState(() => list.map( item => ({ id: generateId(), - ...config.getModelValue(item), + ...getModelValue(item), } as InputModel) ) ); const hasInvalidValues = models.some(config.hasInvalidValuesFn); - const updateValues = (modelList: InputModel[]) => { - setModels(modelList); - onChange(modelList.map(config.onChangeFn)); - }; - const onChangeValue = (index: number, value: string, modelName: string) => { - const { model, isInvalid } = validateValue(value, config); - updateValues( - models.map((range, arrayIndex) => - arrayIndex === index - ? { - ...range, - [modelName]: { - value, - model, - isInvalid, - }, - } - : range - ) - ); - }; - const onDelete = (id: string) => updateValues(models.filter(model => model.id !== id)); - const onAdd = () => - updateValues([ - ...models, - { - id: generateId(), - ...config.getModelValue(), - } as InputModel, - ]); + const updateValues = useCallback( + (modelList: InputModel[]) => { + setModels(modelList); + onChange(modelList.map(onChangeFn)); + }, + [onChangeFn, onChange] + ); + const onChangeValue = useCallback( + (index: number, value: string, modelName: string) => { + const { model, isInvalid } = validateValue(value, validateClass); + updateValues( + models.map((range, arrayIndex) => + arrayIndex === index + ? { + ...range, + [modelName]: { + value, + model, + isInvalid, + }, + } + : range + ) + ); + }, + [models, updateValues, validateClass] + ); + const onDelete = useCallback( + (id: string) => updateValues(models.filter(model => model.id !== id)), + [models, updateValues] + ); + const onAdd = useCallback( + () => + updateValues([ + ...models, + { + id: generateId(), + ...getModelValue(), + } as InputModel, + ]), + [getModelValue, models, updateValues] + ); useEffect(() => { // resposible for setting up an initial value when there is no default value @@ -139,15 +154,15 @@ function InputList({ config, list, onChange, setValidity }: InputListProps) { updateValues([ { id: generateId(), - ...config.defaultValue, + ...defaultValue, } as InputModel, ]); } - }, []); + }, [defaultValue, list.length, updateValues]); useEffect(() => { setValidity(!hasInvalidValues); - }, [hasInvalidValues]); + }, [hasInvalidValues, setValidity]); useEffect(() => { // responsible for discarding changes @@ -155,7 +170,7 @@ function InputList({ config, list, onChange, setValidity }: InputListProps) { list.length !== models.length || list.some((item, index) => { // make model to be the same shape as stored value - const model: InputObject = mapValues(pick(models[index], config.modelNames), 'model'); + const model: InputObject = mapValues(pick(models[index], modelNames), 'model'); // we need to skip empty values since they are not stored in saved object return !isEqual(item, omit(model, isEmpty)); @@ -166,12 +181,12 @@ function InputList({ config, list, onChange, setValidity }: InputListProps) { item => ({ id: generateId(), - ...config.getModelValue(item), + ...getModelValue(item), } as InputModel) ) ); } - }, [list]); + }, [getModelValue, list, modelNames, models]); return ( <> diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/mask_list.tsx similarity index 62% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/mask_list.tsx index f6edecbbcbd701..560213fc08ff07 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/mask_list.tsx @@ -17,12 +17,12 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { InputList, InputListConfig, InputObject, InputModel, InputItem } from './input_list'; -import { search } from '../../../../../../../plugins/data/public'; +import { search } from '../../../../../data/public'; const EMPTY_STRING = ''; @@ -42,36 +42,40 @@ interface MaskListProps { setValidity(isValid: boolean): void; } -function MaskList({ showValidation, onBlur, ...rest }: MaskListProps) { - const maskListConfig: InputListConfig = { - defaultValue: { - mask: { model: '0.0.0.0/1', value: '0.0.0.0/1', isInvalid: false }, +const defaultConfig = { + defaultValue: { + mask: { model: '0.0.0.0/1', value: '0.0.0.0/1', isInvalid: false }, + }, + validateClass: search.aggs.CidrMask, + getModelValue: (item: MaskObject = {}) => ({ + mask: { + model: item.mask || EMPTY_STRING, + value: item.mask || EMPTY_STRING, + isInvalid: false, }, - validateClass: search.aggs.CidrMask, - getModelValue: (item: MaskObject = {}) => ({ - mask: { - model: item.mask || EMPTY_STRING, - value: item.mask || EMPTY_STRING, - isInvalid: false, - }, - }), - getRemoveBtnAriaLabel: (item: MaskModel) => - item.mask.value - ? i18n.translate('visDefaultEditor.controls.ipRanges.removeCidrMaskButtonAriaLabel', { - defaultMessage: 'Remove the CIDR mask value of {mask}', - values: { mask: item.mask.value }, - }) - : i18n.translate('visDefaultEditor.controls.ipRanges.removeEmptyCidrMaskButtonAriaLabel', { - defaultMessage: 'Remove the CIDR mask default value', - }), - onChangeFn: ({ mask }: MaskModel) => { - if (mask.model) { - return { mask: mask.model }; - } - return {}; - }, - hasInvalidValuesFn: ({ mask }) => mask.isInvalid, - renderInputRow: ({ mask }: MaskModel, index, onChangeValue) => ( + }), + getRemoveBtnAriaLabel: (item: MaskModel) => + item.mask.value + ? i18n.translate('visDefaultEditor.controls.ipRanges.removeCidrMaskButtonAriaLabel', { + defaultMessage: 'Remove the CIDR mask value of {mask}', + values: { mask: item.mask.value }, + }) + : i18n.translate('visDefaultEditor.controls.ipRanges.removeEmptyCidrMaskButtonAriaLabel', { + defaultMessage: 'Remove the CIDR mask default value', + }), + onChangeFn: ({ mask }: MaskModel) => { + if (mask.model) { + return { mask: mask.model }; + } + return {}; + }, + hasInvalidValuesFn: ({ mask }: MaskModel) => mask.isInvalid, + modelNames: 'mask', +}; + +function MaskList({ showValidation, onBlur, ...rest }: MaskListProps) { + const renderInputRow = useCallback( + ({ mask }: MaskModel, index, onChangeValue) => ( ), - modelNames: 'mask', + [onBlur, showValidation] + ); + const maskListConfig: InputListConfig = { + ...defaultConfig, + renderInputRow, }; return ; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/index.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/index.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx index a43c66c2e08ccf..b4683e47979cea 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx @@ -79,7 +79,7 @@ function NumberList({ if (!numberArray.length) { onChange([models[0].value as number]); } - }, []); + }, [models, numberArray.length, onChange]); const isValid = !hasInvalidValues(models); useValidation(setValidity, isValid); @@ -109,7 +109,7 @@ function NumberList({ }) ); }, - [numberRange, models, onUpdate] + [models, onUpdate] ); // Add an item to the end of the list diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_row.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_row.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts index e9090e5b38ef78..a19034d3c8e92f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts @@ -108,7 +108,6 @@ describe('Range parsing utility', () => { }; forOwn(tests, (spec, str: any) => { - // eslint-disable-next-line jest/valid-describe describe(str, () => { const range = parseRange(str); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/range.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/range.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx similarity index 95% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx index 6b1a4dca7b84f5..b844fdfb82256b 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx @@ -20,8 +20,8 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DateRangesParamEditor } from './date_ranges'; -import { KibanaContextProvider } from '../../../../../../plugins/kibana_react/public'; -import { docLinksServiceMock } from '../../../../../../core/public/mocks'; +import { KibanaContextProvider } from '../../../../kibana_react/public'; +import { docLinksServiceMock } from '../../../../../core/public/mocks'; describe('DateRangesParamEditor component', () => { let setValue: jest.Mock; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx similarity index 90% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx rename to src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx index 15e864bfd026df..57f4c7d04019b0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, useCallback } from 'react'; import { htmlIdGenerator, EuiButtonIcon, @@ -36,8 +36,9 @@ import dateMath from '@elastic/datemath'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { isEqual, omit } from 'lodash'; +import { useMount } from 'react-use'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../kibana_react/public'; import { AggParamEditorProps } from '../agg_param_props'; const FROM_PLACEHOLDER = '\u2212\u221E'; @@ -72,12 +73,26 @@ function DateRangesParamEditor({ ({ from, to }) => (!from && !to) || !validateDateMath(from) || !validateDateMath(to) ); - // set up an initial range when there is no default range - useEffect(() => { + const updateRanges = useCallback( + (rangeValues: DateRangeValuesModel[]) => { + // do not set internal id parameter into saved object + setValue(rangeValues.map(range => omit(range, 'id'))); + setRanges(rangeValues); + }, + [setValue] + ); + + const onAddRange = useCallback(() => updateRanges([...ranges, { id: generateId() }]), [ + ranges, + updateRanges, + ]); + + useMount(() => { + // set up an initial range when there is no default range if (!value.length) { onAddRange(); } - }, []); + }); useEffect(() => { // responsible for discarding changes @@ -87,18 +102,12 @@ function DateRangesParamEditor({ ) { setRanges(value.map(range => ({ ...range, id: generateId() }))); } - }, [value]); + }, [ranges, value]); useEffect(() => { setValidity(!hasInvalidRange); - }, [hasInvalidRange]); - - const updateRanges = (rangeValues: DateRangeValuesModel[]) => { - // do not set internal id parameter into saved object - setValue(rangeValues.map(range => omit(range, 'id'))); - setRanges(rangeValues); - }; - const onAddRange = () => updateRanges([...ranges, { id: generateId() }]); + }, [hasInvalidRange, setValidity]); + const onRemoveRange = (id: string) => updateRanges(ranges.filter(range => range.id !== id)); const onChangeRange = (id: string, key: string, newValue: string) => updateRanges( diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/drop_partials.tsx b/src/plugins/vis_default_editor/public/components/controls/drop_partials.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/drop_partials.tsx rename to src/plugins/vis_default_editor/public/components/controls/drop_partials.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/extended_bounds.test.tsx b/src/plugins/vis_default_editor/public/components/controls/extended_bounds.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/extended_bounds.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/extended_bounds.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/extended_bounds.tsx b/src/plugins/vis_default_editor/public/components/controls/extended_bounds.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/extended_bounds.tsx rename to src/plugins/vis_default_editor/public/components/controls/extended_bounds.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/field.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx rename to src/plugins/vis_default_editor/public/components/controls/field.tsx index fc1cbb51b70a78..42086004a12dc0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -18,7 +18,8 @@ */ import { get } from 'lodash'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useState, useCallback } from 'react'; +import { useMount } from 'react-use'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -84,8 +85,7 @@ function FieldParamEditor({ const showErrorMessage = (showValidation || !indexedFields.length) && !isValid; useValidation(setValidity, isValid); - - useEffect(() => { + useMount(() => { // set field if only one available if (indexedFields.length !== 1) { return; @@ -98,7 +98,7 @@ function FieldParamEditor({ } else if (indexedField.options.length === 1) { setValue(indexedField.options[0].target); } - }, []); + }); const onSearchChange = useCallback(searchValue => setIsDirty(Boolean(searchValue)), []); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx b/src/plugins/vis_default_editor/public/components/controls/filter.tsx similarity index 99% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx rename to src/plugins/vis_default_editor/public/components/controls/filter.tsx index e2e7c2895093e7..16aaff47f49c31 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filter.tsx @@ -21,7 +21,7 @@ import React, { useState } from 'react'; import { EuiForm, EuiButtonIcon, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IAggConfig, Query, QueryStringInput } from '../../../../../../plugins/data/public'; +import { IAggConfig, Query, QueryStringInput } from '../../../../data/public'; interface FilterRowProps { id: string; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filters.tsx b/src/plugins/vis_default_editor/public/components/controls/filters.tsx similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/filters.tsx rename to src/plugins/vis_default_editor/public/components/controls/filters.tsx index be4c62ab08aa2a..d4e698f655c5e1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filters.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filters.tsx @@ -21,9 +21,10 @@ import React, { useState, useEffect } from 'react'; import { omit, isEqual } from 'lodash'; import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useMount } from 'react-use'; import { Query } from 'src/plugins/data/public'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../kibana_react/public'; import { FilterRow } from './filter'; import { AggParamEditorProps } from '../agg_param_props'; @@ -40,10 +41,10 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps ({ ...filter, id: generateId() })) ); - useEffect(() => { + useMount(() => { // set parsed values into model after initialization setValue(filters.map(filter => omit({ ...filter, input: filter.input }, 'id'))); - }, []); + }); useEffect(() => { // responsible for discarding changes @@ -53,7 +54,7 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps ({ ...filter, id: generateId() }))); } - }, [value]); + }, [filters, value]); const updateFilters = (updatedFilters: FilterValue[]) => { // do not set internal id parameter into saved object diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx b/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx similarity index 73% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx rename to src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx index 90b7cb03b7a5b3..a316a087c8bcbc 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx @@ -17,22 +17,32 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; -import { search } from '../../../../../../plugins/data/public'; +import { search } from '../../../../data/public'; import { SwitchParamEditor } from './switch'; import { AggParamEditorProps } from '../agg_param_props'; const { isType } = search.aggs; function HasExtendedBoundsParamEditor(props: AggParamEditorProps) { + const { agg, setValue, value } = props; + const minDocCount = useRef(agg.params.min_doc_count); + useEffect(() => { - props.setValue(props.value && props.agg.params.min_doc_count); - }, [props.agg.params.min_doc_count]); + if (minDocCount.current !== agg.params.min_doc_count) { + // The "Extend bounds" param is only enabled when "Show empty buckets" is turned on. + // So if "Show empty buckets" is changed, "Extend bounds" should reflect changes + minDocCount.current = agg.params.min_doc_count; + + setValue(value && agg.params.min_doc_count); + } + }, [agg.params.min_doc_count, setValue, value]); return ( ) { !props.agg.params.min_doc_count || !(isType('number')(props.agg) || isType('date')(props.agg)) } - {...props} /> ); } diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/index.ts b/src/plugins/vis_default_editor/public/components/controls/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/index.ts rename to src/plugins/vis_default_editor/public/components/controls/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_range_type.tsx b/src/plugins/vis_default_editor/public/components/controls/ip_range_type.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_range_type.tsx rename to src/plugins/vis_default_editor/public/components/controls/ip_range_type.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/ip_ranges.tsx similarity index 79% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_ranges.tsx rename to src/plugins/vis_default_editor/public/components/controls/ip_ranges.tsx index c4b90649aaaaeb..5ffa8088a95467 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/ip_ranges.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { FromToList, FromToObject } from './components/from_to_list'; @@ -38,12 +38,22 @@ function IpRangesParamEditor({ setValidity, showValidation, }: AggParamEditorProps) { - const handleChange = (modelName: IpRangeTypes, items: Array) => { - setValue({ - ...value, - [modelName]: items, - }); - }; + const handleMaskListChange = useCallback( + (items: MaskObject[]) => + setValue({ + ...value, + [IpRangeTypes.MASK]: items, + }), + [setValue, value] + ); + const handleFromToListChange = useCallback( + (items: FromToObject[]) => + setValue({ + ...value, + [IpRangeTypes.FROM_TO]: items, + }), + [setValue, value] + ); return ( @@ -52,7 +62,7 @@ function IpRangesParamEditor({ list={value.mask} showValidation={showValidation} onBlur={setTouched} - onChange={items => handleChange(IpRangeTypes.MASK, items)} + onChange={handleMaskListChange} setValidity={setValidity} /> ) : ( @@ -60,7 +70,7 @@ function IpRangesParamEditor({ list={value.fromTo} showValidation={showValidation} onBlur={setTouched} - onChange={items => handleChange(IpRangeTypes.FROM_TO, items)} + onChange={handleFromToListChange} setValidity={setValidity} /> )} diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx b/src/plugins/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx rename to src/plugins/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx b/src/plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.tsx rename to src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/min_doc_count.tsx b/src/plugins/vis_default_editor/public/components/controls/min_doc_count.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/min_doc_count.tsx rename to src/plugins/vis_default_editor/public/components/controls/min_doc_count.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx b/src/plugins/vis_default_editor/public/components/controls/missing_bucket.tsx similarity index 93% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx rename to src/plugins/vis_default_editor/public/components/controls/missing_bucket.tsx index 7010f0d53e569e..7d4d2230bd766f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/missing_bucket.tsx @@ -21,20 +21,22 @@ import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { SwitchParamEditor } from './switch'; -import { search } from '../../../../../../plugins/data/public'; +import { search } from '../../../../data/public'; import { AggParamEditorProps } from '../agg_param_props'; function MissingBucketParamEditor(props: AggParamEditorProps) { const fieldTypeIsNotString = !search.aggs.isStringType(props.agg); + const { setValue } = props; useEffect(() => { if (fieldTypeIsNotString) { - props.setValue(false); + setValue(false); } - }, [fieldTypeIsNotString]); + }, [fieldTypeIsNotString, setValue]); return ( ) { } )} disabled={fieldTypeIsNotString} - {...props} /> ); } diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/number_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx similarity index 99% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/number_interval.tsx rename to src/plugins/vis_default_editor/public/components/controls/number_interval.tsx index 6ab5ee2d260a15..02bf680734526c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/number_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx @@ -61,7 +61,7 @@ function NumberIntervalParamEditor({ useEffect(() => { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); const onChange = useCallback( ({ target }: React.ChangeEvent) => diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx b/src/plugins/vis_default_editor/public/components/controls/order.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx rename to src/plugins/vis_default_editor/public/components/controls/order.tsx index 8f63662d928c1a..e609bf9adf790e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order.tsx @@ -39,7 +39,7 @@ function OrderParamEditor({ useEffect(() => { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); // @ts-ignore return ( diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.test.tsx b/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/order_agg.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx rename to src/plugins/vis_default_editor/public/components/controls/order_agg.tsx index 41672bc192fabe..c5a35cbbd7ab1e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_agg.tsx @@ -20,7 +20,7 @@ import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; @@ -47,7 +47,7 @@ function OrderAggParamEditor({ if (orderBy !== 'custom' && value) { setValue(undefined); } - }, [orderBy]); + }, [agg, aggParam, orderBy, setValue, value]); const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( agg, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx b/src/plugins/vis_default_editor/public/components/controls/order_by.tsx similarity index 94% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx rename to src/plugins/vis_default_editor/public/components/controls/order_by.tsx index 9f1aaa54a8ca3e..47b12f4340d429 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_by.tsx @@ -17,9 +17,10 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMount } from 'react-use'; import { isCompatibleAggregation, @@ -28,7 +29,7 @@ import { useValidation, } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; -import { search } from '../../../../../../plugins/data/public'; +import { search } from '../../../../data/public'; const { termsAggFilter } = search.aggs; const DEFAULT_VALUE = '_key'; @@ -58,8 +59,7 @@ function OrderByParamEditor({ const isValid = !!value; useValidation(setValidity, isValid); - - useEffect(() => { + useMount(() => { // setup the initial value of orderBy if (!value) { let respAgg = { id: DEFAULT_VALUE }; @@ -70,7 +70,7 @@ function OrderByParamEditor({ setValue(respAgg.id); } - }, []); + }); useFallbackMetric(setValue, termsAggFilter, metricAggs, value, DEFAULT_VALUE); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/other_bucket.tsx b/src/plugins/vis_default_editor/public/components/controls/other_bucket.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/other_bucket.tsx rename to src/plugins/vis_default_editor/public/components/controls/other_bucket.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx b/src/plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx rename to src/plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx b/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx b/src/plugins/vis_default_editor/public/components/controls/percentiles.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx rename to src/plugins/vis_default_editor/public/components/controls/percentiles.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/precision.tsx b/src/plugins/vis_default_editor/public/components/controls/precision.tsx similarity index 95% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/precision.tsx rename to src/plugins/vis_default_editor/public/components/controls/precision.tsx index df71e72ee6c617..ad2011513b171c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/precision.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/precision.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { EuiRange, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../kibana_react/public'; import { AggParamEditorProps } from '../agg_param_props'; function PrecisionParamEditor({ agg, value, setValue }: AggParamEditorProps) { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx similarity index 95% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx rename to src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx index c64b079e4f802c..86c4431b6d5ed1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx @@ -17,10 +17,11 @@ * under the License. */ -import React, { useEffect, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { EuiFormRow, EuiIconTip, EuiRange, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useMount } from 'react-use'; import { AggControlProps } from './agg_control_props'; @@ -44,11 +45,11 @@ function RadiusRatioOptionControl({ editorStateParams, setStateParamValue }: Agg ); - useEffect(() => { + useMount(() => { if (!editorStateParams.radiusRatio) { setStateParamValue(PARAM_NAME, DEFAULT_VALUE); } - }, []); + }); const onChange = useCallback( (e: React.ChangeEvent | React.MouseEvent) => diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/range_control.tsx b/src/plugins/vis_default_editor/public/components/controls/range_control.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/range_control.tsx rename to src/plugins/vis_default_editor/public/components/controls/range_control.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/ranges.tsx similarity index 91% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/ranges.tsx rename to src/plugins/vis_default_editor/public/components/controls/ranges.tsx index 27de9dfe68ee0a..5c6438b4004085 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/ranges.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useCallback, useState, useEffect } from 'react'; import { htmlIdGenerator, EuiButtonIcon, @@ -76,13 +76,44 @@ function RangesParamEditor({ validateRange, }: RangesParamEditorProps) { const [ranges, setRanges] = useState(() => value.map(range => ({ ...range, id: generateId() }))); + const updateRanges = useCallback( + (rangeValues: RangeValuesModel[]) => { + // do not set internal id parameter into saved object + setValue(rangeValues.map(range => omit(range, 'id'))); + setRanges(rangeValues); + + if (setTouched) { + setTouched(true); + } + }, + [setTouched, setValue] + ); + const onAddRange = useCallback( + () => + addRangeValues + ? updateRanges([...ranges, { ...addRangeValues(), id: generateId() }]) + : updateRanges([...ranges, { id: generateId() }]), + [addRangeValues, ranges, updateRanges] + ); + const onRemoveRange = (id: string) => updateRanges(ranges.filter(range => range.id !== id)); + const onChangeRange = (id: string, key: string, newValue: string) => + updateRanges( + ranges.map(range => + range.id === id + ? { + ...range, + [key]: newValue === '' ? undefined : parseFloat(newValue), + } + : range + ) + ); // set up an initial range when there is no default range useEffect(() => { if (!value.length) { onAddRange(); } - }, []); + }, [onAddRange, value.length]); useEffect(() => { // responsible for discarding changes @@ -92,33 +123,7 @@ function RangesParamEditor({ ) { setRanges(value.map(range => ({ ...range, id: generateId() }))); } - }, [value]); - - const updateRanges = (rangeValues: RangeValuesModel[]) => { - // do not set internal id parameter into saved object - setValue(rangeValues.map(range => omit(range, 'id'))); - setRanges(rangeValues); - - if (setTouched) { - setTouched(true); - } - }; - const onAddRange = () => - addRangeValues - ? updateRanges([...ranges, { ...addRangeValues(), id: generateId() }]) - : updateRanges([...ranges, { id: generateId() }]); - const onRemoveRange = (id: string) => updateRanges(ranges.filter(range => range.id !== id)); - const onChangeRange = (id: string, key: string, newValue: string) => - updateRanges( - ranges.map(range => - range.id === id - ? { - ...range, - [key]: newValue === '' ? undefined : parseFloat(newValue), - } - : range - ) - ); + }, [ranges, value]); const hasInvalidRange = validateRange && diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/raw_json.tsx b/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/raw_json.tsx rename to src/plugins/vis_default_editor/public/components/controls/raw_json.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/rows_or_columns.tsx b/src/plugins/vis_default_editor/public/components/controls/rows_or_columns.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/rows_or_columns.tsx rename to src/plugins/vis_default_editor/public/components/controls/rows_or_columns.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/scale_metrics.tsx b/src/plugins/vis_default_editor/public/components/controls/scale_metrics.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/scale_metrics.tsx rename to src/plugins/vis_default_editor/public/components/controls/scale_metrics.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/size.test.tsx b/src/plugins/vis_default_editor/public/components/controls/size.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/size.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/size.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/size.tsx b/src/plugins/vis_default_editor/public/components/controls/size.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/size.tsx rename to src/plugins/vis_default_editor/public/components/controls/size.tsx index 824ec4aeb253c2..9f55eb9212dee7 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/size.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/size.tsx @@ -48,7 +48,7 @@ function SizeParamEditor({ useEffect(() => { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); return ( { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); const onChange = useCallback(ev => setValue(ev.target.value), [setValue]); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_agg.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx rename to src/plugins/vis_default_editor/public/components/controls/sub_agg.tsx index c9f53a68b3e83e..ee0fbd8532ce93 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_agg.tsx @@ -20,7 +20,7 @@ import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; @@ -29,7 +29,6 @@ function SubAggParamEditor({ agg, aggParam, formIsTouched, - value, metricAggs, state, setValue, @@ -44,7 +43,7 @@ function SubAggParamEditor({ } else if (!agg.params.customMetric) { setValue(aggParam.makeAgg(agg)); } - }, [value, metricAggs]); + }, [metricAggs, agg, setValue, aggParam]); const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( agg, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx rename to src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx index ead3f8bb006231..361eeba9abdbf8 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx @@ -17,11 +17,12 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { EuiFormLabel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMount } from 'react-use'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; @@ -48,13 +49,13 @@ function SubMetricParamEditor({ const aggTitle = type === 'customMetric' ? metricTitle : bucketTitle; const aggGroup = type === 'customMetric' ? AggGroupNames.Metrics : AggGroupNames.Buckets; - useEffect(() => { + useMount(() => { if (agg.params[type]) { setValue(agg.params[type]); } else { setValue(aggParam.makeAgg(agg)); } - }, []); + }); const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( agg, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/switch.tsx b/src/plugins/vis_default_editor/public/components/controls/switch.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/switch.tsx rename to src/plugins/vis_default_editor/public/components/controls/switch.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/test_utils.ts b/src/plugins/vis_default_editor/public/components/controls/test_utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/test_utils.ts rename to src/plugins/vis_default_editor/public/components/controls/test_utils.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx rename to src/plugins/vis_default_editor/public/components/controls/time_interval.tsx index 971a62faf7d7c9..4af41f67bc524c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx @@ -23,7 +23,7 @@ import { EuiFormRow, EuiIconTip, EuiComboBox, EuiComboBoxOptionOption } from '@e import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { search, AggParamOption } from '../../../../../../plugins/data/public'; +import { search, AggParamOption } from '../../../../data/public'; import { AggParamEditorProps } from '../agg_param_props'; const { parseEsInterval, InvalidEsCalendarIntervalError } = search.aggs; @@ -161,7 +161,7 @@ function TimeIntervalParamEditor({ useEffect(() => { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); return ( { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); useEffect(() => { if (isFirstRun.current) { @@ -102,7 +102,7 @@ export function TopAggregateParamEditor({ if (filteredOptions.length === 1) { setValue(aggParam.options.find(opt => opt.value === filteredOptions[0].value)); } - }, [fieldType]); + }, [aggParam.options, fieldType, filteredOptions, setValue, value]); const handleChange = (event: React.ChangeEvent) => { if (event.target.value === emptyValue.value) { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_field.tsx b/src/plugins/vis_default_editor/public/components/controls/top_field.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/top_field.tsx rename to src/plugins/vis_default_editor/public/components/controls/top_field.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_size.tsx b/src/plugins/vis_default_editor/public/components/controls/top_size.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/top_size.tsx rename to src/plugins/vis_default_editor/public/components/controls/top_size.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_sort_field.tsx b/src/plugins/vis_default_editor/public/components/controls/top_sort_field.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/top_sort_field.tsx rename to src/plugins/vis_default_editor/public/components/controls/top_sort_field.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/use_geocentroid.tsx b/src/plugins/vis_default_editor/public/components/controls/use_geocentroid.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/use_geocentroid.tsx rename to src/plugins/vis_default_editor/public/components/controls/use_geocentroid.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts b/src/plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/index.ts b/src/plugins/vis_default_editor/public/components/controls/utils/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/index.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx b/src/plugins/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx rename to src/plugins/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/index.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/index.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/prose.test.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/prose.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/prose.test.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/prose.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/prose.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/prose.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/prose.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/prose.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts b/src/plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/controls.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/controls.tsx index 18b445b4a26dbd..db9d7d9e3316a1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/controls.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx @@ -30,7 +30,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useDebounce } from 'react-use'; -import { Vis } from '../../../../../../plugins/visualizations/public'; +import { Vis } from 'src/plugins/visualizations/public'; import { discardChanges, EditorAction } from './state'; interface DefaultEditorControlsProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx b/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx index 0c967723db8e71..0466c64541e23b 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx @@ -26,7 +26,8 @@ import { IAggConfig, IMetricAggType, search, -} from '../../../../../../plugins/data/public'; + TimeRange, +} from '../../../../data/public'; import { DefaultEditorAggGroup } from '../agg_group'; import { EditorAction, @@ -39,7 +40,6 @@ import { } from './state'; import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from '../agg_common_props'; import { ISchemas } from '../../schemas'; -import { TimeRange } from '../../../../../../plugins/data/public'; import { EditorVisState } from './state/reducers'; export interface DefaultEditorDataTabProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/index.ts b/src/plugins/vis_default_editor/public/components/sidebar/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/index.ts rename to src/plugins/vis_default_editor/public/components/sidebar/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/navbar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/navbar.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/navbar.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/navbar.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index b24486a12fd246..9dfeae1815d1a7 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -23,16 +23,15 @@ import { i18n } from '@kbn/i18n'; import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EventEmitter } from 'events'; -import { Vis } from 'src/plugins/visualizations/public'; +import { Vis, PersistedState } from 'src/plugins/visualizations/public'; +import { SavedSearch } from 'src/plugins/discover/public'; +import { TimeRange } from 'src/plugins/data/public'; import { DefaultEditorNavBar, OptionTab } from './navbar'; import { DefaultEditorControls } from './controls'; import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; import { SidebarTitle } from './sidebar_title'; -import { PersistedState } from '../../../../../../plugins/visualizations/public'; -import { SavedSearch } from '../../../../../../plugins/discover/public'; import { Schema } from '../../schemas'; -import { TimeRange } from '../../../../../../plugins/data/public'; interface DefaultEditorSideBarProps { isCollapsed: boolean; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx index fb63a598a4faec..c9f83e5b474a68 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx @@ -35,8 +35,8 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { Vis } from '../../../../../../plugins/visualizations/public'; -import { SavedSearch } from '../../../../../../plugins/discover/public'; +import { Vis } from 'src/plugins/visualizations/public'; +import { SavedSearch } from 'src/plugins/discover/public'; interface LinkedSearchProps { savedSearch: SavedSearch; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/actions.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/actions.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/actions.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/actions.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/constants.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/constants.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/constants.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/constants.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/editor_form_state.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/editor_form_state.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/editor_form_state.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/editor_form_state.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/index.ts similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/index.ts index d39d6d07b32d28..1bfa100cbd0f7a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/state/index.ts @@ -24,7 +24,7 @@ import { Vis } from 'src/plugins/visualizations/public'; import { createEditorStateReducer, initEditorState, EditorVisState } from './reducers'; import { EditorStateActionTypes } from './constants'; import { EditorAction } from './actions'; -import { useKibana } from '../../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../../kibana_react/public'; import { VisDefaultEditorKibanaServices } from '../../../types'; export * from './editor_form_state'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts similarity index 99% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts index b9f89cebd8bf34..4e7a2904584daf 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts @@ -20,7 +20,7 @@ import { cloneDeep } from 'lodash'; import { Vis } from 'src/plugins/visualizations/public'; -import { AggGroupNames, DataPublicPluginStart } from '../../../../../../../plugins/data/public'; +import { AggGroupNames, DataPublicPluginStart } from '../../../../../data/public'; import { EditorStateActionTypes } from './constants'; import { getEnabledMetricAggsCount } from '../../agg_group_helper'; import { EditorAction } from './actions'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/utils/editor_config.ts b/src/plugins/vis_default_editor/public/components/utils/editor_config.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/utils/editor_config.ts rename to src/plugins/vis_default_editor/public/components/utils/editor_config.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/utils/index.ts b/src/plugins/vis_default_editor/public/components/utils/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/utils/index.ts rename to src/plugins/vis_default_editor/public/components/utils/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx similarity index 94% rename from src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx rename to src/plugins/vis_default_editor/public/default_editor.tsx index 899b9c1b5fd6eb..f1963b94dcf954 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -19,8 +19,8 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { EditorRenderProps } from '../../kibana/public/visualize/np_ready/types'; -import { PanelsContainer, Panel } from '../../../../plugins/kibana_react/public'; +import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; +import { PanelsContainer, Panel } from '../../kibana_react/public'; import './vis_type_agg_filter'; import { DefaultEditorSideBar } from './components/sidebar'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx similarity index 92% rename from src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx rename to src/plugins/vis_default_editor/public/default_editor_controller.tsx index 58e67b5064da51..798da09f8e30bf 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -23,9 +23,9 @@ import { i18n } from '@kbn/i18n'; import { EventEmitter } from 'events'; import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; -import { Vis, VisualizeEmbeddableContract } from '../../../../plugins/visualizations/public'; -import { Storage } from '../../../../plugins/kibana_utils/public'; -import { KibanaContextProvider } from '../../../../plugins/kibana_react/public'; +import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { Storage } from '../../kibana_utils/public'; +import { KibanaContextProvider } from '../../kibana_react/public'; import { DefaultEditor } from './default_editor'; import { DefaultEditorDataTab, OptionTab } from './components/sidebar'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/editor_size.ts b/src/plugins/vis_default_editor/public/editor_size.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/editor_size.ts rename to src/plugins/vis_default_editor/public/editor_size.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/index.scss b/src/plugins/vis_default_editor/public/index.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/index.scss rename to src/plugins/vis_default_editor/public/index.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/index.ts b/src/plugins/vis_default_editor/public/index.ts similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/index.ts rename to src/plugins/vis_default_editor/public/index.ts index 156d50f451b577..6c58c6df26b001 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/index.ts +++ b/src/plugins/vis_default_editor/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + export { DefaultEditorController } from './default_editor_controller'; export { useValidation } from './components/controls/utils'; export { RangesParamEditor, RangeValues } from './components/controls/ranges'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/schemas.ts b/src/plugins/vis_default_editor/public/schemas.ts similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/schemas.ts rename to src/plugins/vis_default_editor/public/schemas.ts index 4e632da44afc08..05ba5fa9c94198 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/schemas.ts +++ b/src/plugins/vis_default_editor/public/schemas.ts @@ -21,7 +21,7 @@ import _, { defaults } from 'lodash'; import { Optional } from '@kbn/utility-types'; -import { AggGroupNames, AggParam, IAggGroupNames } from '../../../../plugins/data/public'; +import { AggGroupNames, AggParam, IAggGroupNames } from '../../data/public'; export interface ISchemas { [AggGroupNames.Buckets]: Schema[]; diff --git a/src/legacy/core_plugins/vis_default_editor/public/types.ts b/src/plugins/vis_default_editor/public/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/types.ts rename to src/plugins/vis_default_editor/public/types.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/utils.test.ts b/src/plugins/vis_default_editor/public/utils.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/utils.test.ts rename to src/plugins/vis_default_editor/public/utils.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/utils.ts b/src/plugins/vis_default_editor/public/utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/utils.ts rename to src/plugins/vis_default_editor/public/utils.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx b/src/plugins/vis_default_editor/public/vis_options_props.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx rename to src/plugins/vis_default_editor/public/vis_options_props.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts b/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts rename to src/plugins/vis_default_editor/public/vis_type_agg_filter.ts index 3ff212c43e6e84..bf5661f42a9f5f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts +++ b/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { IAggType, IAggConfig, IndexPattern, search } from '../../../../plugins/data/public'; +import { IAggType, IAggConfig, IndexPattern, search } from '../../data/public'; const { aggTypeFilters, propFilter } = search.aggs; const filterByName = propFilter('name'); diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 53b7e7062ee2d4..2f9d9f9bfb178f 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 00000000000000..ed68b41e8c89ab --- /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/services/browser.ts b/test/functional/services/browser.ts index 5017947e95d03b..13d2365c07191b 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 157918df874c82..8b57ecd3c82350 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 46d81f1737a55f..aa6e364d0a09d0 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 e571a1a7e55512..b0724488cb5db8 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 3bf5b865aa7ba5..fc0b5bbb787c81 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/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx index d3f66d708603c5..6c430e34d5e29b 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; -import { VisOptionsProps } from '../../../../../../src/legacy/core_plugins/vis_default_editor/public/vis_options_props'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public/vis_options_props'; interface CounterParams { counter: number; 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 205a303bcf47b0..afa0cb51cd1083 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 b26833c02fe22c..250b9a5d188d0e 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 21bedc204f48b7..e4b656ae8160d7 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/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx index b1959e4d68aa43..30c772bf5f634a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx @@ -90,6 +90,7 @@ function FormRow({ onChange( setting.key, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx index 52befe37ffdae9..bd00bcf600ffeb 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx @@ -3,14 +3,26 @@ * 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 { EuiLink, EuiText } from '@elastic/eui'; import Mustache from 'mustache'; +import React from 'react'; +import styled from 'styled-components'; import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { - SectionLinks, - SectionLink -} from '../../../../../../../../plugins/observability/public'; +import { px, truncate, units } from '../../../../style/variables'; + +const LinkContainer = styled.li` + margin-top: ${px(units.half)}; + &:first-of-type { + margin-top: 0; + } +`; + +const TruncateText = styled(EuiText)` + font-weight: 500; + line-height: ${px(units.unit)}; + ${truncate(px(units.unit * 25))} +`; export const CustomLinkSection = ({ customLinks, @@ -19,7 +31,7 @@ export const CustomLinkSection = ({ customLinks: CustomLink[]; transaction: Transaction; }) => ( - +
    {customLinks.map(link => { let href = link.url; try { @@ -28,13 +40,12 @@ export const CustomLinkSection = ({ // ignores any error that happens } return ( - + + + {link.label} + + ); })} - +
); diff --git a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js index 85cb6d45c595da..cc74faeac6a964 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js @@ -6,6 +6,7 @@ const path = require('path'); const webpack = require('webpack'); +const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); @@ -73,7 +74,20 @@ module.exports = async ({ config }) => { path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), }, }, - { loader: 'sass-loader' }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + )};\n`; + }, + sassOptions: { + includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], + }, + }, + }, ], }); @@ -86,8 +100,9 @@ module.exports = async ({ config }) => { loader: 'css-loader', options: { importLoaders: 2, - modules: true, - localIdentName: '[name]__[local]___[hash:base64:5]', + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, }, }, { @@ -159,7 +174,11 @@ module.exports = async ({ config }) => { // what require() calls it will execute within the bundle JSON.stringify({ type, modules: extensions[type] || [] }), ].join(''); - }) + }), + + // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform + new webpack.NormalModuleReplacementPlugin(/lib\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), + new webpack.NormalModuleReplacementPlugin(/lib\/download_workpad/, path.resolve(__dirname, '../tasks/mocks/downloadWorkpad')), ); // Tell Webpack about relevant extensions diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/demo_rows_types.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/demo_rows_types.ts new file mode 100644 index 00000000000000..e92dc79fba8c31 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/demo_rows_types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum DemoRows { + CI = 'ci', + SHIRTS = 'shirts', +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts index 02f8efcfde95d3..58a2354b5cf384 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts @@ -6,14 +6,10 @@ import { cloneDeep } from 'lodash'; import ci from './ci.json'; +import { DemoRows } from './demo_rows_types'; import shirts from './shirts.json'; import { getFunctionErrors } from '../../../../i18n'; -export enum DemoRows { - CI = 'ci', - SHIRTS = 'shirts', -} - export function getDemoRows(arg: string | null) { if (arg === DemoRows.CI) { return cloneDeep(ci); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts index 826c49d328f214..5cebae5bb669f7 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts @@ -8,7 +8,8 @@ import { sortBy } from 'lodash'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; // @ts-ignore unconverted lib file import { queryDatatable } from '../../../../common/lib/datatable/query'; -import { DemoRows, getDemoRows } from './get_demo_rows'; +import { DemoRows } from './demo_rows_types'; +import { getDemoRows } from './get_demo_rows'; import { Filter, Datatable, DatatableColumn, DatatableRow } from '../../../../types'; import { getFunctionHelp } from '../../../../i18n'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.ts index eba0b47f7dc13b..88d2b904e6cb3f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.ts @@ -7,7 +7,7 @@ import { applyTemplateStrings } from '../../i18n/templates'; import darkTemplate from './theme_dark.json'; import lightTemplate from './theme_light.json'; -import pitchTemplate from './pitch_presentation.json'; +// import pitchTemplate from './pitch_presentation.json'; import statusTemplate from './status_report.json'; import summaryTemplate from './summary_report.json'; @@ -15,7 +15,7 @@ import summaryTemplate from './summary_report.json'; export const templateSpecs = applyTemplateStrings([ darkTemplate, lightTemplate, - pitchTemplate, + // pitchTemplate, statusTemplate, summaryTemplate, ]); diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts index caedbfdec5be4c..35a5b86f752dce 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { demodata } from '../../../canvas_plugin_src/functions/server/demodata'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { DemoRows } from '../../../canvas_plugin_src/functions/server/demodata/get_demo_rows'; +import { DemoRows } from '../../../canvas_plugin_src/functions/server/demodata/demo_rows_types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.demodataHelpText', { diff --git a/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.ts b/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.ts index 5ab6a908641dea..d8e4d51706be96 100644 --- a/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.ts +++ b/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.ts @@ -37,14 +37,6 @@ export const getTemplateStrings = (): TemplateStringDict => ({ defaultMessage: 'Light color themed presentation deck', }), }, - Pitch: { - name: i18n.translate('xpack.canvas.templates.pitchName', { - defaultMessage: 'Pitch', - }), - help: i18n.translate('xpack.canvas.templates.pitchHelp', { - defaultMessage: 'Branded presentation with large photos', - }), - }, Status: { name: i18n.translate('xpack.canvas.templates.statusName', { defaultMessage: 'Status', @@ -62,3 +54,14 @@ export const getTemplateStrings = (): TemplateStringDict => ({ }), }, }); + +export const getUnusedTemplateStrings = (): TemplateStringDict => ({ + Pitch: { + name: i18n.translate('xpack.canvas.templates.pitchName', { + defaultMessage: 'Pitch', + }), + help: i18n.translate('xpack.canvas.templates.pitchHelp', { + defaultMessage: 'Branded presentation with large photos', + }), + }, +}); diff --git a/x-pack/legacy/plugins/canvas/tasks/mocks/downloadWorkpad.js b/x-pack/legacy/plugins/canvas/tasks/mocks/downloadWorkpad.js new file mode 100644 index 00000000000000..3571448c11aa98 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/downloadWorkpad.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const downloadWorkpad = async workpadId => console.log(`Download workpad ${workpadId}`); + +export const downloadRenderedWorkpad = async renderedWorkpad => + console.log(`Download workpad ${renderedWorkpad.id}`); + +export const downloadRuntime = async basePath => console.log(`Download run time at ${basePath}`); + +export const downloadZippedRuntime = async data => console.log(`Downloading data ${data}`); 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 9ebaee57fba4db..e319535b4a45ca 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 e73a51ffa2ced9..8bad536b39245a 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 161b5b81c12554..8f11d1b23376c9 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/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts index 9a4030f3eb2147..3a4c7b71dcd03d 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -239,11 +239,15 @@ export const ALERT_TYPE_PREFIX = 'monitoring_'; * This is the alert type id for the license expiration alert */ export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; +/** + * This is the alert type id for the cluster state alert + */ +export const ALERT_TYPE_CLUSTER_STATE = `${ALERT_TYPE_PREFIX}alert_type_cluster_state`; /** * A listing of all alert types */ -export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION]; +export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_STATE]; /** * Matches the id for the built-in in email action type @@ -254,7 +258,7 @@ export const ALERT_ACTION_TYPE_EMAIL = '.email'; /** * The number of alerts that have been migrated */ -export const NUMBER_OF_MIGRATED_ALERTS = 1; +export const NUMBER_OF_MIGRATED_ALERTS = 2; /** * The advanced settings config name for the email address diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js index 11fcef73a4b976..95c1af5549198f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js @@ -6,10 +6,15 @@ import React from 'react'; import chrome from '../../np_imports/ui/chrome'; -import { capitalize } from 'lodash'; +import { capitalize, get } from 'lodash'; import { formatDateTimeLocal } from '../../../common/formatting'; import { formatTimestampToDuration } from '../../../common'; -import { CALCULATE_DURATION_SINCE, EUI_SORT_DESCENDING } from '../../../common/constants'; +import { + CALCULATE_DURATION_SINCE, + EUI_SORT_DESCENDING, + ALERT_TYPE_LICENSE_EXPIRATION, + ALERT_TYPE_CLUSTER_STATE, +} from '../../../common/constants'; import { mapSeverity } from './map_severity'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; @@ -21,6 +26,8 @@ const linkToCategories = { 'elasticsearch/indices': 'Elasticsearch Indices', 'kibana/instances': 'Kibana Instances', 'logstash/instances': 'Logstash Nodes', + [ALERT_TYPE_LICENSE_EXPIRATION]: 'License expiration', + [ALERT_TYPE_CLUSTER_STATE]: 'Cluster state', }; const getColumns = (kbnUrl, scope, timezone) => [ { @@ -94,19 +101,22 @@ const getColumns = (kbnUrl, scope, timezone) => [ }), field: 'message', sortable: true, - render: (message, alert) => ( - { - scope.$evalAsync(() => { - kbnUrl.changePath(target); - }); - }} - /> - ), + render: (_message, alert) => { + const message = get(alert, 'message.text', get(alert, 'message', '')); + return ( + { + scope.$evalAsync(() => { + kbnUrl.changePath(target); + }); + }} + /> + ); + }, }, { name: i18n.translate('xpack.monitoring.alerts.categoryColumnTitle', { @@ -148,8 +158,8 @@ const getColumns = (kbnUrl, scope, timezone) => [ export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) => { const alertsFlattened = alerts.map(alert => ({ ...alert, - status: alert.metadata.severity, - category: alert.metadata.link, + status: get(alert, 'metadata.severity', get(alert, 'severity', 0)), + category: get(alert, 'metadata.link', get(alert, 'type', null)), })); const injector = chrome.dangerouslyGetActiveInjector(); 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 94d951a94fe29a..cb1081c0c14da6 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 0933cd22db7c9f..eaa474ba177b18 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 650294c29e9a50..19a1a61d00a424 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/monitoring/public/components/alerts/status.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx index 258a5b68db372e..d3cf4b463a2cc5 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { kfetch } from 'ui/kfetch'; import { AlertsStatus, AlertsStatusProps } from './status'; -import { ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { ALERT_TYPES } from '../../../common/constants'; import { getSetupModeState } from '../../lib/setup_mode'; import { mockUseEffects } from '../../jest.helpers'; @@ -63,11 +63,7 @@ describe('Status', () => { it('should render a success message if all alerts have been migrated and in setup mode', async () => { (kfetch as jest.Mock).mockReturnValue({ - data: [ - { - alertTypeId: ALERT_TYPE_PREFIX, - }, - ], + data: ALERT_TYPES.map(type => ({ alertTypeId: type })), }); (getSetupModeState as jest.Mock).mockReturnValue({ diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx index 072a98b1234521..5f5329bf7fff83 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx @@ -142,7 +142,7 @@ export const AlertsStatus: React.FC = (props: AlertsStatusPro ); } - const allMigrated = kibanaAlerts.length === NUMBER_OF_MIGRATED_ALERTS; + const allMigrated = kibanaAlerts.length >= NUMBER_OF_MIGRATED_ALERTS; if (allMigrated) { if (setupModeEnabled) { return ( diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index 8455fb8cf3088c..d87ff98e79be00 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -6,14 +6,12 @@ import React, { Fragment } from 'react'; import moment from 'moment-timezone'; -import chrome from '../../../np_imports/ui/chrome'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; import { CALCULATE_DURATION_SINCE, KIBANA_ALERTING_ENABLED, - ALERT_TYPE_LICENSE_EXPIRATION, CALCULATE_DURATION_UNTIL, } from '../../../../common/constants'; import { formatDateTimeLocal } from '../../../../common/formatting'; @@ -31,6 +29,37 @@ import { EuiLink, } from '@elastic/eui'; +function replaceTokens(alert) { + if (!alert.message.tokens) { + return alert.message.text; + } + + let text = alert.message.text; + + for (const token of alert.message.tokens) { + if (token.type === 'time') { + text = text.replace( + token.startToken, + token.isRelative + ? formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL) + : moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z') + ); + } else if (token.type === 'link') { + const linkPart = new RegExp(`${token.startToken}(.+?)${token.endToken}`).exec(text); + // TODO: we assume this is at the end, which works for now but will not always work + const nonLinkText = text.replace(linkPart[0], ''); + text = ( + + {nonLinkText} + {linkPart[1]} + + ); + } + } + + return text; +} + export function AlertsPanel({ alerts, changeUrl }) { const goToAlerts = () => changeUrl('/alerts'); @@ -58,9 +87,6 @@ export function AlertsPanel({ alerts, changeUrl }) { severityIcon.iconType = 'check'; } - const injector = chrome.dangerouslyGetActiveInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); - return ( @@ -96,14 +122,7 @@ export function AlertsPanel({ alerts, changeUrl }) { const alertsList = KIBANA_ALERTING_ENABLED ? alerts.map((alert, idx) => { const callOutProps = mapSeverity(alert.severity); - let message = alert.message - // scan message prefix and replace relative times - // \w: Matches any alphanumeric character from the basic Latin alphabet, including the underscore. Equivalent to [A-Za-z0-9_]. - .replace( - '#relative', - formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL) - ) - .replace('#absolute', moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z')); + const message = replaceTokens(alert); if (!alert.isFiring) { callOutProps.title = i18n.translate( @@ -118,22 +137,30 @@ export function AlertsPanel({ alerts, changeUrl }) { ); callOutProps.color = 'success'; callOutProps.iconType = 'check'; - } else { - if (alert.type === ALERT_TYPE_LICENSE_EXPIRATION) { - message = ( - - {message} -   - Please update your license - - ); - } } return ( - -

{message}

-
+ + +

{message}

+ +

+ +

+
+
+ +
); }) : alerts.map((item, index) => ( diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js index 7c065a78a8af98..62cc985887e9f5 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js @@ -18,25 +18,37 @@ import { Alerts } from '../../components/alerts'; import { MonitoringViewBaseEuiTableController } from '../base_eui_table_controller'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink } from '@elastic/eui'; -import { CODE_PATH_ALERTS } from '../../../common/constants'; +import { CODE_PATH_ALERTS, KIBANA_ALERTING_ENABLED } from '../../../common/constants'; function getPageData($injector) { const globalState = $injector.get('globalState'); const $http = $injector.get('$http'); const Private = $injector.get('Private'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; + const url = KIBANA_ALERTING_ENABLED + ? `../api/monitoring/v1/alert_status` + : `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; const timeBounds = timefilter.getBounds(); + const data = { + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }; + + if (!KIBANA_ALERTING_ENABLED) { + data.ccs = globalState.ccs; + } return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, + .post(url, data) + .then(response => { + const result = get(response, 'data', []); + if (KIBANA_ALERTING_ENABLED) { + return result.alerts; + } + return result; }) - .then(response => get(response, 'data', [])) .catch(err => { const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); diff --git a/x-pack/legacy/plugins/rollup/kibana.json b/x-pack/legacy/plugins/rollup/kibana.json index 3781d59d8c0f36..3df8bd7c187d5e 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 f0eb21a219442f..f4de2a30981276 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 fbf2612b83aa82..809a76d1868b22 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 ec530e63408f45..83945110c2c760 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 c58975419e20f1..5782e88c3448bf 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 662fb8fb8ef68e..22f1b3beffa35d 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/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts index 1559285d508ed2..2d2db9e70255b2 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -32,7 +32,7 @@ describe('Signal detection rules', () => { esArchiverUnload('prebuilt_rules_loaded'); }); - it('Sorts by activated rules', () => { + it.skip('Sorts by activated rules', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS); goToManageSignalDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); 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 ed47cdc62a1b67..c24081c777a968 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 d31dcdbee2a146..30108ecf33874a 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/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index bc559c5ac49721..f89d21ef1aeb19 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -70,7 +70,7 @@ const MetaRule = t.intersection([ }), t.partial({ throttle: t.string, - kibanaSiemAppUrl: t.string, + kibana_siem_app_url: t.string, }), ]); 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 a3df3664398ad2..135f0f2a7e26d7 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/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts index efb601b6bd2072..8d793f39afa997 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts @@ -515,7 +515,7 @@ describe('helpers', () => { actions: [], enabled: false, meta: { - kibanaSiemAppUrl: 'http://localhost:5601/app/siem', + kibana_siem_app_url: 'http://localhost:5601/app/siem', }, throttle: 'no_actions', }; @@ -533,7 +533,7 @@ describe('helpers', () => { actions: [], enabled: false, meta: { - kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, throttle: 'no_actions', }; @@ -566,7 +566,7 @@ describe('helpers', () => { ], enabled: false, meta: { - kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, throttle: 'rule', }; @@ -599,7 +599,7 @@ describe('helpers', () => { ], enabled: false, meta: { - kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, throttle: mockStepData.throttle, }; @@ -631,7 +631,7 @@ describe('helpers', () => { ], enabled: false, meta: { - kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, throttle: 'no_actions', }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index 151e3a9bdf4d6d..7ad116c313361d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -155,7 +155,7 @@ export const formatActionsStepData = (actionsStepData: ActionsStepRule): Actions enabled, throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, meta: { - kibanaSiemAppUrl, + kibana_siem_app_url: kibanaSiemAppUrl, }, }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index db1f2298b5ea78..58a1b0fd3133e6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -64,7 +64,7 @@ export const getActionsStepsData = ( actions: actions?.map(transformRuleToAlertAction), isNew: false, throttle, - kibanaSiemAppUrl: meta?.kibanaSiemAppUrl, + kibanaSiemAppUrl: meta?.kibana_siem_app_url, enabled, }; }; 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 6955e57d099be3..14b2e1ae9e366c 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/build_signals_query.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts index 189c596a771256..c6923283bec16f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts @@ -41,6 +41,7 @@ describe('buildSignalsSearchQuery', () => { '@timestamp': { gt: from, lte: to, + format: 'epoch_millis', }, }, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts index b973d4c5f4e98b..be0943c014225b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts @@ -32,6 +32,7 @@ export const buildSignalsSearchQuery = ({ ruleId, index, from, to }: BuildSignal '@timestamp': { gt: from, lte: to, + format: 'epoch_millis', }, }, }, 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 073251b68f414c..3878f5dae88898 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 3a1697f1c8afc6..ccd7576255d83d 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 546488caa5ee78..e4ad53de742d61 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'); @@ -53,7 +55,7 @@ export const rulesNotificationAlertType = ({ from: fromInMs, to: toInMs, index: ruleParams.outputIndex, - ruleId: ruleParams.ruleId!, + ruleId: ruleParams.ruleId, callCluster: services.callCluster, }); @@ -61,14 +63,14 @@ export const rulesNotificationAlertType = ({ from: fromInMs, to: toInMs, id: ruleAlertSavedObject.id, - kibanaSiemAppUrl: ruleAlertParams.meta?.kibanaSiemAppUrl as string, + kibanaSiemAppUrl: ruleAlertParams.meta?.kibana_siem_app_url, }); logger.info( `Found ${signalsCount} signals using signal rule name: "${ruleParams.name}", id: "${params.ruleAlertId}", rule_id: "${ruleParams.ruleId}" in "${ruleParams.outputIndex}" index` ); - if (signalsCount) { + if (signalsCount !== 0) { const alertInstance = services.alertInstanceFactory(alertId); scheduleNotificationActions({ alertInstance, signalsCount, resultsLink, ruleParams }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts index 749b892ef506f3..9f145af79ca90f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -8,7 +8,7 @@ import { mapKeys, snakeCase } from 'lodash/fp'; import { AlertInstance } from '../../../../../../../plugins/alerting/server'; import { RuleTypeParams } from '../types'; -type NotificationRuleTypeParams = RuleTypeParams & { +export type NotificationRuleTypeParams = RuleTypeParams & { name: string; id: string; }; 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 128a7965cd7dc5..32a8737adc7c9c 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 4c077dd9fc1fb0..e1f7526438c31a 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 3197d21c0e95a7..ac0de406aceb29 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/notifications/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts index 5dc7e7fc30b7fa..c91c4490e8eba0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts @@ -10,7 +10,7 @@ export const getNotificationResultsLink = ({ from, to, }: { - kibanaSiemAppUrl: string; + kibanaSiemAppUrl?: string; id: string; from?: string; to?: string; 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 3b24b722f73561..e400360a5a5b25 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 = () => ({ @@ -450,25 +451,31 @@ export const getResult = (): RuleAlertType => ({ lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -512,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/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts index 63fe51f2c81cdc..c929b0718207d6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -141,25 +141,31 @@ export const getOutputRuleAlertForRest = (): Omit< lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, 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 d0e36515946a89..5377e9039785e8 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 6038ad2095323a..9a329b78b8f129 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 43e970702ba72e..29ae5056a3ae8f 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/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 85255594ee4805..8c0fceb7a5f295 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -121,15 +121,15 @@ export const patchRulesBulkRoute = (router: IRouter) => { anomalyThreshold, machineLearningJobId, }); - if (rule != null) { + if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, alertsClient, savedObjectsClient, - enabled: rule.enabled!, + enabled: rule.enabled, actions, throttle, - name: rule.name!, + name: rule.name, }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index f553ccd2c6f81f..9c5000d70e5fe8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -117,15 +117,15 @@ export const patchRulesRoute = (router: IRouter) => { anomalyThreshold, machineLearningJobId, }); - if (rule != null) { + if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, alertsClient, savedObjectsClient, - enabled: rule.enabled!, + enabled: rule.enabled, actions, throttle, - name: rule.name!, + name: rule.name, }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes 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 9916972f418433..36e15780f5cb3f 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 21dd2a4429cca5..0444c757a9b316 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/routes/rules/validate.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 77e05796fbcbe4..7537401e5a3667 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -74,25 +74,31 @@ export const ruleOutput: RulesSchema = { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index b10627d151fa29..8c741c937bf159 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -1561,25 +1561,31 @@ describe('add prepackaged rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 08bd01ee9a1a00..e56e8e5fe34d31 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -1526,25 +1526,31 @@ describe('create rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index c8e5bb981f921b..40f7b19ea12b3e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -1747,25 +1747,31 @@ describe('import rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index 45b5028f392b9b..e01a8f40fcea4a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -1229,25 +1229,31 @@ describe('patch rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -1263,25 +1269,28 @@ describe('patch rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts index 46cd1b653b5b44..d5ea950d163f57 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -66,25 +66,31 @@ export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesS lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts index 538c8f754fd6ea..faae1dde835452 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -154,13 +154,34 @@ export const note = t.string; // NOTE: Experimental list support not being shipped currently and behind a feature flag // TODO: Remove this comment once we lists have passed testing and is ready for the release -export const boolean_operator = t.keyof({ and: null, 'and not': null }); -export const list_type = t.keyof({ value: null }); // TODO: (LIST-FEATURE) Eventually this can include "list" when we support lists CRUD -export const list_value = t.exact(t.type({ name: t.string, type: list_type })); +export const list_field = t.string; +export const list_values_operator = t.keyof({ included: null, excluded: null }); +export const list_values_type = t.keyof({ match: null, match_all: null, list: null, exists: null }); +export const list_values = t.exact( + t.intersection([ + t.type({ + name: t.string, + }), + t.partial({ + id: t.string, + description: t.string, + created_at, + }), + ]) +); export const list = t.exact( - t.type({ - field: t.string, - boolean_operator, - values: t.array(list_value), - }) + t.intersection([ + t.type({ + field: t.string, + values_operator: list_values_operator, + values_type: list_values_type, + }), + t.partial({ values: t.array(list_values) }), + ]) ); +export const list_and = t.intersection([ + list, + t.partial({ + and: t.array(list), + }), +]); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index 16e419f389f09e..c17ae8466a86c5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -126,12 +126,28 @@ export const note = Joi.string(); // NOTE: Experimental list support not being shipped currently and behind a feature flag // TODO: (LIST-FEATURE) Remove this comment once we lists have passed testing and is ready for the release -export const boolean_operator = Joi.string().valid('and', 'and not'); -export const list_type = Joi.string().valid('value'); // TODO: (LIST-FEATURE) Eventually this can be "list" when we support list types -export const list_value = Joi.object({ name: Joi.string().required(), type: list_type.required() }); +export const list_field = Joi.string(); +export const list_values_operator = Joi.string().valid(['included', 'excluded']); +export const list_values_types = Joi.string().valid(['match', 'match_all', 'list', 'exists']); +export const list_values = Joi.object({ + name: Joi.string().required(), + id: Joi.string(), + description: Joi.string(), + created_at, +}); export const list = Joi.object({ - field: Joi.string().required(), - boolean_operator: boolean_operator.required(), - values: Joi.array().items(list_value), + field: list_field.required(), + values_operator: list_values_operator.required(), + values_type: list_values_types.required(), + values: Joi.when('values_type', { + is: 'exists', + then: Joi.forbidden(), + otherwise: Joi.array() + .items(list_values) + .required(), + }), +}); +export const list_and = Joi.object({ + and: Joi.array().items(list), }); -export const lists = Joi.array().items(list); +export const lists = Joi.array().items(list.concat(list_and)); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts index 14df1c3d8cd55a..e8a9c7b0886a19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts @@ -23,25 +23,79 @@ describe('lists_default_array', () => { const payload = [ { field: 'source.ip', - boolean_operator: 'and', + values_operator: 'included', + values_type: 'exists', + }, + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match', values: [ { - name: '127.0.0.1', - type: 'value', + name: 'rock01', + }, + ], + and: [ + { + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array of lists that includes a values_operator other than included or excluded', () => { + const payload = [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'exists', + }, + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'exists', + }, + { + field: 'host.hostname', + values_operator: 'jibber jabber', + values_type: 'exists', + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "jibber jabber" supplied to "values_operator"', + ]); + expect(message.schema).toEqual({}); + }); + + // TODO - this scenario should never come up, as the values key is forbidden when values_type is "exists" in the incoming schema - need to find a good way to do this in io-ts + test('it will validate an array of lists that includes "values" when "values_type" is "exists"', () => { + const payload = [ { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'exists', values: [ { - name: 'rock01', - type: 'value', - }, - { - name: 'mothra', - type: 'value', + name: '127.0.0.1', }, ], }, @@ -53,15 +107,63 @@ describe('lists_default_array', () => { expect(message.schema).toEqual(payload); }); + // TODO - this scenario should never come up, as the values key is required when values_type is "match" in the incoming schema - need to find a good way to do this in io-ts + test('it will validate an array of lists that does not include "values" when "values_type" is "match"', () => { + const payload = [ + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match', + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + // TODO - this scenario should never come up, as the values key is required when values_type is "match_all" in the incoming schema - need to find a good way to do this in io-ts + test('it will validate an array of lists that does not include "values" when "values_type" is "match_all"', () => { + const payload = [ + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match_all', + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + // TODO - this scenario should never come up, as the values key is required when values_type is "list" in the incoming schema - need to find a good way to do this in io-ts + test('it should not validate an array of lists that does not include "values" when "values_type" is "list"', () => { + const payload = [ + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'list', + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + test('it should not validate an array with a number', () => { const payload = [ { field: 'source.ip', - boolean_operator: 'and', + values_operator: 'included', + values_type: 'exists', values: [ { name: '127.0.0.1', - type: 'value', }, ], }, @@ -70,7 +172,10 @@ describe('lists_default_array', () => { const decoded = ListsDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to ""', + 'Invalid value "5" supplied to ""', + ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts index 0e0944a11b4166..85a38e296494a1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts @@ -7,10 +7,10 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { list } from '../response/schemas'; +import { list_and as listAnd } from '../response/schemas'; export type ListsDefaultArrayC = t.Type; -type List = t.TypeOf; +type List = t.TypeOf; /** * Types the ListsDefaultArray as: @@ -18,9 +18,9 @@ type List = t.TypeOf; */ export const ListsDefaultArray: ListsDefaultArrayC = new t.Type( 'listsWithDefaultArray', - t.array(list).is, + t.array(listAnd).is, (input): Either => - input == null ? t.success([]) : t.array(list).decode(input), + input == null ? t.success([]) : t.array(listAnd).decode(input), t.identity ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index 6f6beea7fa5fbd..e8f9aad620ca01 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -1552,25 +1552,28 @@ describe('create rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 8d7360bae8eb9c..e4015ad8bafa4d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -309,7 +309,7 @@ export const validateLicenseForRuleType = ({ }: { license: ILicense; ruleType: RuleType; -}) => { +}): void => { if (isMlRule(ruleType) && !license.hasAtLeast(MINIMUM_ML_LICENSE)) { const message = i18n.translate('xpack.siem.licensing.unsupportedMachineLearningMessage', { defaultMessage: 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 23c99b36cb4a76..991690d901d8ae 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,12 +9,13 @@ 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; savedObjectsClient: AlertServices['savedObjectsClient']; actions: RuleAlertAction[] | undefined; - throttle: string | undefined; + throttle: string | null | undefined; } export const createRuleActionsSavedObject = async ({ @@ -22,7 +23,7 @@ export const createRuleActionsSavedObject = async ({ savedObjectsClient, actions = [], throttle, -}: CreateRuleActionsSavedObject) => { +}: 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 4e8781dd456920..91489334940bd1 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 @@ -16,10 +16,11 @@ interface DeleteRuleActionsSavedObject { export const deleteRuleActionsSavedObject = async ({ ruleAlertId, savedObjectsClient, -}: DeleteRuleActionsSavedObject) => { +}: 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 3ae9090526d693..dad35f6cb1f96e 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 @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; @@ -14,10 +15,17 @@ interface GetRuleActionsSavedObject { savedObjectsClient: AlertServices['savedObjectsClient']; } +export interface RulesActionsSavedObject { + id: string; + actions: RuleAlertAction[]; + alertThrottle: string | null; + ruleThrottle: string; +} + export const getRuleActionsSavedObject = async ({ ruleAlertId, savedObjectsClient, -}: GetRuleActionsSavedObject) => { +}: GetRuleActionsSavedObject): Promise => { const { saved_objects } = await savedObjectsClient.find< IRuleActionsAttributesSavedObjectAttributes >({ @@ -29,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 3856f75255262a..d79c61f6200e3d 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 @@ -15,7 +15,7 @@ interface UpdateOrCreateRuleActionsSavedObject { ruleAlertId: string; savedObjectsClient: AlertServices['savedObjectsClient']; actions: RuleAlertAction[] | undefined; - throttle: string | undefined; + throttle: string | null | undefined; } export const updateOrCreateRuleActionsSavedObject = async ({ @@ -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 56bce3c8b67a36..2a2c84838ed93c 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'; @@ -15,7 +15,8 @@ interface DeleteRuleActionsSavedObject { ruleAlertId: string; savedObjectsClient: AlertServices['savedObjectsClient']; actions: RuleAlertAction[] | undefined; - throttle: string | undefined; + throttle: string | null | undefined; + ruleActions: RulesActionsSavedObject; } export const updateRuleActionsSavedObject = async ({ @@ -23,11 +24,8 @@ export const updateRuleActionsSavedObject = async ({ savedObjectsClient, actions, throttle, -}: DeleteRuleActionsSavedObject) => { - 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/rule_actions/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts index 3c297ed8485552..554c2b4a3dd905 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts @@ -5,16 +5,27 @@ */ import { SavedObjectsUpdateResponse } from 'kibana/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; -export const getThrottleOptions = (throttle = 'no_actions') => ({ - ruleThrottle: throttle, - alertThrottle: ['no_actions', 'rule'].includes(throttle) ? null : throttle, +export const getThrottleOptions = ( + throttle: string | undefined | null = 'no_actions' +): { + ruleThrottle: string; + alertThrottle: string | null; +} => ({ + ruleThrottle: throttle ?? 'no_actions', + alertThrottle: ['no_actions', 'rule'].includes(throttle ?? 'no_actions') ? null : throttle, }); export const getRuleActionsFromSavedObject = ( savedObject: SavedObjectsUpdateResponse -) => ({ +): { + id: string; + actions: RuleAlertAction[]; + alertThrottle: string | null; + ruleThrottle: string; +} => ({ id: savedObject.id, actions: savedObject.attributes.actions || [], alertThrottle: savedObject.attributes.alertThrottle || null, 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 4c8d0f51f251bd..a60f1d41779782 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 bebf4f350483b6..91effb4741b8b0 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/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index ca6fb15e1fad90..dd004e3685b1d7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -82,25 +82,31 @@ describe('getExportAll', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 175c906f7996cf..715cb23e8444a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -90,25 +90,31 @@ describe('get_export_by_object_ids', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -212,25 +218,31 @@ describe('get_export_by_object_ids', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, 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 bcbe460fb6a66c..6d4bacb9cc243c 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 d7655a15499eb7..5c4889ec5fd683 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 38b1097a845f8b..b1bed5d7161556 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 ac10143c1d8d07..00000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts +++ /dev/null @@ -1,50 +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 } 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) => { - 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 ca299db6ace50e..72f4cbcbe68e80 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 0e70e05f4de78c..99326768ed33b9 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 f70c591243a765..994a54048b71a3 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,14 +8,14 @@ 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 { alertsClient: AlertsClient; savedObjectsClient: AlertServices['savedObjectsClient']; ruleAlertId: string; actions: RuleAlertAction[] | undefined; - throttle: string | undefined; + throttle: string | null | undefined; enabled: boolean; name: string; } @@ -28,7 +28,7 @@ export const updateRulesNotifications = async ({ enabled, name, throttle, -}: UpdateRulesNotifications) => { +}: UpdateRulesNotifications): Promise => { const ruleActions = await updateOrCreateRuleActionsSavedObject({ savedObjectsClient, ruleAlertId, @@ -36,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 7804439ce07345..750c5574f4a729 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/scripts/rules/patches/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json index 8c86f4c85af1d8..4db8724db4e13d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json @@ -3,21 +3,28 @@ "lists": [ { "field": "source.ip", - "boolean_operator": "and", - "values": [ - { - "name": "127.0.0.1", - "type": "value" - } - ] + "values_operator": "excluded", + "values_type": "exists" }, { "field": "host.name", - "boolean_operator": "and not", + "values_operator": "included", + "values_type": "match", "values": [ { - "name": "rock01", - "type": "value" + "name": "rock01" + } + ], + "and": [ + { + "field": "host.id", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "123456" + } + ] } ] } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json index f6856eec59966d..997d03369a699a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json @@ -9,25 +9,31 @@ "lists": [ { "field": "source.ip", - "boolean_operator": "and", - "values": [ - { - "name": "127.0.0.1", - "type": "value" - } - ] + "values_operator": "included", + "values_type": "exists" }, { "field": "host.name", - "boolean_operator": "and not", + "values_operator": "excluded", + "values_type": "match", "values": [ { - "name": "rock01", - "type": "value" - }, + "name": "rock01" + } + ], + "and": [ { - "name": "mothra", - "type": "value" + "field": "host.id", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "123" + }, + { + "name": "678" + } + ] } ] } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json index 6704c9676fa562..66b198974f5743 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json @@ -9,21 +9,28 @@ "lists": [ { "field": "source.ip", - "boolean_operator": "and", - "values": [ - { - "name": "127.0.0.1", - "type": "value" - } - ] + "values_operator": "excluded", + "values_type": "exists" }, { "field": "host.name", - "boolean_operator": "and not", + "values_operator": "included", + "values_type": "match", "values": [ { - "name": "rock01", - "type": "value" + "name": "rock01" + } + ], + "and": [ + { + "field": "host.id", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "123456" + } + ] } ] } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6d7d7e93d7e6e0..7a211c5631da6f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -47,25 +47,31 @@ export const sampleRuleAlertParams = ( lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index f2c2b99bdac8c9..f1729e35ce1f07 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -93,25 +93,31 @@ describe('buildBulkBody', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -213,25 +219,31 @@ describe('buildBulkBody', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -331,25 +343,31 @@ describe('buildBulkBody', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -442,25 +460,31 @@ describe('buildBulkBody', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts index e360ceaf02f4dc..e5183ed4df7bd9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -82,25 +82,31 @@ describe('buildRule', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -159,25 +165,31 @@ describe('buildRule', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -235,25 +247,31 @@ describe('buildRule', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, 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 355041d9efbdb5..ba8938f116fc6b 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 414270ffcdd5c8..81600b0b8dd9b6 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 ff81730bc4a72b..3a964cb91fbdb7 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 11d31f1805440f..03fb5832fdf42e 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 @@ -47,7 +47,7 @@ const getPayload = ( interval: ruleAlert.schedule.interval, name: ruleAlert.name, tags: ruleAlert.tags, - throttle: ruleAlert.throttle!, + throttle: ruleAlert.throttle, scrollSize: 10, scrollLock: '0', }, @@ -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 417fcbbe42a565..0357f906f8035a 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 @@ -24,7 +24,10 @@ import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; -import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; +import { + scheduleNotificationActions, + NotificationRuleTypeParams, +} from '../notifications/schedule_notification_actions'; import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; @@ -95,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, @@ -178,6 +182,7 @@ export const signalRulesAlertType = ({ updatedAt, interval, enabled, + refresh, tags, }); result.success = success; @@ -238,6 +243,7 @@ export const signalRulesAlertType = ({ interval, enabled, pageSize: searchAfterSize, + refresh, tags, throttle, }); @@ -246,7 +252,7 @@ export const signalRulesAlertType = ({ if (result.success) { if (actions.length) { - const notificationRuleParams = { + const notificationRuleParams: NotificationRuleTypeParams = { ...ruleParams, name, id: savedObject.id, @@ -259,7 +265,7 @@ export const signalRulesAlertType = ({ from: fromInMs, to: toInMs, id: savedObject.id, - kibanaSiemAppUrl: meta?.kibanaSiemAppUrl as string, + kibanaSiemAppUrl: meta?.kibana_siem_app_url, }); logger.info( 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 56f061cdfa3ca9..45365b446cbf0e 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 6dd8823b57e4de..fc33d0e15e43fe 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 543e8bf0619b00..040e32aa0d360d 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: RuleAlertParams; + 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 efa0a92cc573b5..035f1b10ff8b2c 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 @@ -29,6 +29,11 @@ export interface ThreatParams { // We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove // types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. +export interface Meta { + [key: string]: {} | string | undefined | null; + kibana_siem_app_url?: string | undefined; +} + export interface RuleAlertParams { actions: RuleAlertAction[]; anomalyThreshold: number | undefined; @@ -51,7 +56,7 @@ export interface RuleAlertParams { query: string | undefined | null; references: string[]; savedId?: string | undefined | null; - meta: Record | undefined | null; + meta: Meta | undefined | null; severity: string; tags: string[]; to: string; @@ -60,7 +65,7 @@ export interface RuleAlertParams { threat: ThreatParams[] | undefined | null; type: RuleType; version: number; - throttle: string; + throttle: string | undefined | null; lists: ListsDefaultArraySchema | null | undefined; } @@ -144,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/tilemap/index.js b/x-pack/legacy/plugins/tilemap/index.js index 767a0fe72985e8..d4105519ee0a77 100644 --- a/x-pack/legacy/plugins/tilemap/index.js +++ b/x-pack/legacy/plugins/tilemap/index.js @@ -15,7 +15,7 @@ export const tilemap = kibana => { require: ['xpack_main', 'vis_type_vislib'], publicDir: resolve(__dirname, 'public'), uiExports: { - visTypeEnhancers: ['plugins/tilemap/vis_type_enhancers/update_tilemap_settings'], + hacks: ['plugins/tilemap/vis_type_enhancers/update_tilemap_settings'], }, init: function(server) { const thisPlugin = this; 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 b547f8b076f931..a49468ad3dd066 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 5143e1c9639048..b86e85f35b17d4 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 d217d26e84836b..82cc09f5e9eca6 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 f3042a701211f7..61b338d47b9f50 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 8a39d68f40bb6e..431bfb1e99c3bd 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 0df07ad58fb9e3..955e1569380a5d 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 129829850f9c1b..8f73bfb31ea4de 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 67b7553c4a7361..51e87dbd75b48d 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 9e4795be6c2082..1f04efd1941b4b 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 eee2ae352fe3d0..88553c314112f3 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 00000000000000..884353e132b9c5 --- /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 75396f2aad897d..bc4268bb698723 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 383f84590fbc6d..6215b08df81d4f 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 ce31e62bc9b8e8..34c9e7aa9e8b8f 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 cddebb3a8e31e3..ffd1f0faabbabc 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 1b130421fa71fa..00000000000000 --- 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 45b967629a2a85..00000000000000 --- 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 00000000000000..6499427b8c1a53 --- /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 00000000000000..c70a13bc01c9f8 --- /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 33191132bece55..94f9ec1c943646 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 999e739e77060c..92e38d77314f83 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 9d4ea69a636090..2574e73dd4f9ae 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 0e929ff457fbd2..a9ff5ee8ecdc62 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 5538b44b69fcbb..6f8478df58a533 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 4c74ca54a0d2f4..951d18a33b35f1 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 fd480658e236ac..734417e72733e3 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 90e274df3a5ee8..fdca6c0a9b503c 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 9efe020bc10c4b..42c83a7c04deba 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 29c2f3c5730f4c..fa2358a1f181ce 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 5de82a9ee8788e..54dd4704edfc0a 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/amount_and_unit.ts b/x-pack/plugins/apm/common/agent_configuration/amount_and_unit.ts index 447e529c9c199a..d6520ae1505398 100644 --- a/x-pack/plugins/apm/common/agent_configuration/amount_and_unit.ts +++ b/x-pack/plugins/apm/common/agent_configuration/amount_and_unit.ts @@ -10,7 +10,8 @@ interface AmountAndUnit { } export function amountAndUnitToObject(value: string): AmountAndUnit { - const [, amount = '', unit = ''] = value.match(/(\d+)?(\w+)?/) || []; + // matches any postive and negative number and its unit. + const [, amount = '', unit = ''] = value.match(/(^-?\d+)?(\w+)?/) || []; return { amount, unit }; } diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.test.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.test.ts index a83ee9262cad67..98d0cb5f028c33 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.test.ts @@ -10,24 +10,56 @@ * you may not use this file except in compliance with the Elastic License. */ -import { durationRt } from './duration_rt'; +import { durationRt, getDurationRt } from './duration_rt'; import { isRight } from 'fp-ts/lib/Either'; describe('durationRt', () => { describe('it should not accept', () => { - [undefined, null, '', 0, 'foo', true, false, '100', 's', 'm', '0h'].map( + [ + undefined, + null, + '', + 0, + 'foo', + true, + false, + '100', + 's', + 'm', + '0ms', + '-1ms' + ].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(durationRt.decode(input))).toBe(false); + }); + }); + }); + + describe('it should accept', () => { + ['1000ms', '2s', '3m', '1s'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(durationRt.decode(input))).toBe(true); + }); + }); + }); +}); + +describe('getDurationRt', () => { + const customDurationRt = getDurationRt({ min: -1 }); + describe('it should not accept', () => { + [undefined, null, '', 0, 'foo', true, false, '100', 's', 'm', '-2ms'].map( input => { it(`${JSON.stringify(input)}`, () => { - expect(isRight(durationRt.decode(input))).toBe(false); + expect(isRight(customDurationRt.decode(input))).toBe(false); }); } ); }); - describe('It should accept', () => { - ['1000ms', '2s', '3m'].map(input => { + describe('it should accept', () => { + ['1000ms', '2s', '3m', '1s', '-1s', '0ms'].map(input => { it(`${JSON.stringify(input)}`, () => { - expect(isRight(durationRt.decode(input))).toBe(true); + expect(isRight(customDurationRt.decode(input))).toBe(true); }); }); }); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.ts index 383fd69be9a78e..b691276854fb0a 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.ts @@ -10,24 +10,28 @@ import { amountAndUnitToObject } from '../amount_and_unit'; export const DURATION_UNITS = ['ms', 's', 'm']; -export const durationRt = new t.Type( - 'durationRt', - t.string.is, - (input, context) => { - return either.chain(t.string.validate(input, context), inputAsString => { - const { amount, unit } = amountAndUnitToObject(inputAsString); - const amountAsInt = parseInt(amount, 10); - const isValidUnit = DURATION_UNITS.includes(unit); - const isValid = amountAsInt > 0 && isValidUnit; +export function getDurationRt({ min }: { min: number }) { + return new t.Type( + 'durationRt', + t.string.is, + (input, context) => { + return either.chain(t.string.validate(input, context), inputAsString => { + const { amount, unit } = amountAndUnitToObject(inputAsString); + const amountAsInt = parseInt(amount, 10); + const isValidUnit = DURATION_UNITS.includes(unit); + const isValid = amountAsInt >= min && isValidUnit; - return isValid - ? t.success(inputAsString) - : t.failure( - input, - context, - `Must have numeric amount and a valid unit (${DURATION_UNITS})` - ); - }); - }, - t.identity -); + return isValid + ? t.success(inputAsString) + : t.failure( + input, + context, + `Must have numeric amount and a valid unit (${DURATION_UNITS})` + ); + }); + }, + t.identity + ); +} + +export const durationRt = getDurationRt({ min: 1 }); 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 4b74b07fc8e27d..49840d2157af76 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 @@ -118,6 +118,7 @@ Array [ }, Object { "key": "span_frames_min_duration", + "min": -1, "type": "duration", "units": Array [ "ms", @@ -152,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/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index 152db37a1bff32..e73aed35e87f94 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { getIntegerRt } from '../runtime_types/integer_rt'; import { captureBodyRt } from '../runtime_types/capture_body_rt'; import { RawSettingDefinition } from './types'; +import { getDurationRt } from '../runtime_types/duration_rt'; /* * Settings added here will show up in the UI and will be validated on the client and server @@ -62,7 +63,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.captureBody.description', { defaultMessage: - 'For transactions that are HTTP requests, the agent can optionally capture the request body (e.g. POST variables).' + 'For transactions that are HTTP requests, the agent can optionally capture the request body (e.g. POST variables).\nFor transactions that are initiated by receiving a message from a message broker, the agent can capture the textual message body.' } ), options: [ @@ -86,7 +87,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.captureHeaders.description', { defaultMessage: - 'If set to `true`, the agent will capture request and response headers, including cookies.\n\nNOTE: Setting this to `false` reduces network bandwidth, disk space and object allocations.' + 'If set to `true`, the agent will capture HTTP request and response headers (including cookies), as well as message headers/properties when using messaging frameworks (like Kafka).\n\nNOTE: Setting this to `false` reduces network bandwidth, disk space and object allocations.' } ), excludeAgents: ['js-base', 'rum-js', 'nodejs'] @@ -116,7 +117,7 @@ export const generalSettings: RawSettingDefinition[] = [ }), description: i18n.translate('xpack.apm.agentConfig.recording.description', { defaultMessage: - 'When recording, the agent instruments incoming HTTP requests, tracks errors, and collects and sends metrics. When inactive, the agent works as a noop, not collecting data and not communicating with the APM Server except for polling for updated configuration. As this is a reversible switch, agent threads are not being killed when inactivated, but they will be mostly idle in this state, so the overhead should be negligible. You can use this setting to dynamically control whether Elastic APM is enabled or disabled.' + 'When recording, the agent instruments incoming HTTP requests, tracks errors, and collects and sends metrics. When set to non-recording, the agent works as a noop, not collecting data and not communicating with the APM Server except for polling for updated configuration. As this is a reversible switch, agent threads are not being killed when set to non-recording, but they will be mostly idle in this state, so the overhead should be negligible. You can use this setting to dynamically control whether Elastic APM is enabled or disabled.' }), excludeAgents: ['nodejs'] }, @@ -143,6 +144,7 @@ export const generalSettings: RawSettingDefinition[] = [ { key: 'span_frames_min_duration', type: 'duration', + validation: getDurationRt({ min: -1 }), defaultValue: '5ms', label: i18n.translate('xpack.apm.agentConfig.spanFramesMinDuration.label', { defaultMessage: 'Span frames minimum duration' @@ -154,7 +156,8 @@ export const generalSettings: RawSettingDefinition[] = [ 'In its default settings, the APM agent will collect a stack trace with every recorded span.\nWhile this is very helpful to find the exact place in your code that causes the span, collecting this stack trace does have some overhead. \nWhen setting this option to a negative value, like `-1ms`, stack traces will be collected for all spans. Setting it to a positive value, e.g. `5ms`, will limit stack trace collection to spans with durations equal to or longer than the given value, e.g. 5 milliseconds.\n\nTo disable stack trace collection for spans completely, set the value to `0ms`.' } ), - excludeAgents: ['js-base', 'rum-js', 'nodejs'] + excludeAgents: ['js-base', 'rum-js', 'nodejs'], + min: -1 }, // STACK_TRACE_LIMIT @@ -212,7 +215,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.transactionSampleRate.description', { defaultMessage: - 'By default, the agent will sample every transaction (e.g. request to your service). To reduce overhead and storage requirements, you can set the sample rate to a value between 0.0 and 1.0. We still record overall time and the result for unsampled transactions, but no context information, labels, or spans.' + 'By default, the agent will sample every transaction (e.g. request to your service). To reduce overhead and storage requirements, you can set the sample rate to a value between 0.0 and 1.0. We still record overall time and the result for unsampled transactions, but not context information, labels, or spans.' } ) } 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 bb050076b9f9a9..2e10c743785494 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/agent_configuration/setting_definitions/types.d.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts index 6b584fc7e20486..282ced346dda05 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts @@ -91,6 +91,7 @@ interface BytesSetting extends BaseSetting { interface DurationSetting extends BaseSetting { type: 'duration'; units?: string[]; + min?: number; } export type RawSettingDefinition = diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index bc1b346f50da78..d5c3f91eb92470 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 b9ac9d5431700d..982ad558dc91d5 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 8ea6df5a9898af..5221d737866f48 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 9b210c2aa05adc..d92af587d0e92b 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/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 0ec105129b7ac4..7c24bd9d771483 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -7,6 +7,11 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields } from './types'; +// FIXME: move types/model to top-level +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { PolicyData } from '../public/applications/endpoint/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { generatePolicy } from '../public/applications/endpoint/models/policy'; export type Event = AlertEvent | EndpointEvent; @@ -452,6 +457,39 @@ export class EndpointDocGenerator { } } + /** + * Generates an Ingest `datasource` that includes the Endpoint Policy data + */ + public generatePolicyDatasource(): PolicyData { + return { + id: this.seededUUIDv4(), + name: 'Endpoint Policy', + description: 'Policy to protect the worlds data', + config_id: this.seededUUIDv4(), + enabled: true, + output_id: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: generatePolicy(), + }, + }, + }, + ], + namespace: 'default', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '1.0.0', + }, + revision: 1, + }; + } + private randomN(n: number): number { return Math.floor(this.random() * n); } diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index e8e1281a889253..a614526d92a3f9 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -86,7 +86,7 @@ export interface AlertResultList { export interface HostResultList { /* the hosts restricted by the page size */ - hosts: HostMetadata[]; + hosts: HostInfo[]; /* the total number of unique hosts in the index */ total: number; /* the page size requested */ @@ -252,6 +252,32 @@ export type AlertData = AlertEvent & AlertMetadata; export type AlertDetails = AlertData & AlertState; +/** + * The status of the host + */ +export enum HostStatus { + /** + * Default state of the host when no host information is present or host information cannot + * be retrieved. e.g. API error + */ + ERROR = 'error', + + /** + * Host is online as indicated by its checkin status during the last checkin window + */ + ONLINE = 'online', + + /** + * Host is offline as indicated by its checkin status during the last checkin window + */ + OFFLINE = 'offline', +} + +export type HostInfo = Immutable<{ + metadata: HostMetadata; + host_status: HostStatus; +}>; + export type HostMetadata = Immutable<{ '@timestamp': number; event: { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index fa9055e0d9bbd3..89a6302351a546 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -7,13 +7,9 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { CoreStart, AppMountParameters, ScopedHistory } from 'kibana/public'; -import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; -import { Route, Switch, Router } from 'react-router-dom'; -import { Provider } from 'react-redux'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Route, Switch } from 'react-router-dom'; import { Store } from 'redux'; -import { useObservable } from 'react-use'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { RouteCapture } from './view/route_capture'; import { EndpointPluginStartDependencies } from '../../plugin'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; @@ -21,7 +17,7 @@ import { HostList } from './view/hosts'; import { PolicyList } from './view/policy'; import { PolicyDetails } from './view/policy'; import { HeaderNavigation } from './components/header_nav'; -import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components'; +import { AppRootProvider } from './view/app_root_provider'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -49,54 +45,31 @@ interface RouterProps { } const AppRoot: React.FunctionComponent = React.memo( - ({ - history, - store, - coreStart: { http, notifications, uiSettings, application }, - depsStart: { data }, - }) => { - const isDarkMode = useObservable(uiSettings.get$('theme:darkMode')); - + ({ history, store, coreStart, depsStart }) => { return ( - - - - - - - - - ( -

- -

- )} - /> - - - - - ( - - )} - /> -
-
-
-
-
-
-
+ + + + ( +

+ +

+ )} + /> + + + + + ( + + )} + /> +
+
); } ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx new file mode 100644 index 00000000000000..af34205e2310f3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx @@ -0,0 +1,70 @@ +/* + * 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 from 'react'; +import { createMemoryHistory } from 'history'; +import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react'; +import { appStoreFactory } from '../store'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { EndpointPluginStartDependencies } from '../../../plugin'; +import { depsStartMock } from './dependencies_start_mock'; +import { AppRootProvider } from '../view/app_root_provider'; + +type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; + +/** + * Mocked app root context renderer + */ +interface AppContextTestRender { + store: ReturnType; + history: ReturnType; + coreStart: ReturnType; + depsStart: EndpointPluginStartDependencies; + /** + * A wrapper around `AppRootContext` component. Uses the mocked modules as input to the + * `AppRootContext` + */ + AppWrapper: React.FC; + /** + * Renders the given UI within the created `AppWrapper` providing the given UI a mocked + * endpoint runtime context environment + */ + render: UiRender; +} + +/** + * Creates a mocked endpoint app context custom renderer that can be used to render + * component that depend upon the application's surrounding context providers. + * Factory also returns the content that was used to create the custom renderer, allowing + * for further customization. + */ +export const createAppRootMockRenderer = (): AppContextTestRender => { + const history = createMemoryHistory(); + const coreStart = coreMock.createStart({ basePath: '/mock' }); + const depsStart = depsStartMock(); + const store = appStoreFactory({ coreStart, depsStart }); + const AppWrapper: React.FunctionComponent<{ children: React.ReactElement }> = ({ children }) => ( + + {children} + + ); + const render: UiRender = (ui, options) => { + // @ts-ignore + return reactRender(ui, { + wrapper: AppWrapper, + ...options, + }); + }; + + return { + store, + history, + coreStart, + depsStart, + AppWrapper, + render, + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/mocks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/dependencies_start_mock.ts similarity index 96% rename from x-pack/plugins/endpoint/public/applications/endpoint/mocks.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/mocks/dependencies_start_mock.ts index e1a90b4a416dc2..00cf0bca57e66f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/mocks.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/dependencies_start_mock.ts @@ -7,7 +7,7 @@ import { dataPluginMock, Start as DataPublicStartMock, -} from '../../../../../../src/plugins/data/public/mocks'; +} from '../../../../../../../src/plugins/data/public/mocks'; type DataMock = Omit & { indexPatterns: Omit & { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/index.ts new file mode 100644 index 00000000000000..65e78f27943ba0 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './dependencies_start_mock'; +export * from './app_context_render'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts index dee35aa3b895a2..4dafa68ddb647d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts @@ -5,7 +5,7 @@ */ import { HostListPagination, ServerApiError } from '../../types'; -import { HostResultList, HostMetadata } from '../../../../../common/types'; +import { HostResultList, HostInfo } from '../../../../../common/types'; interface ServerReturnedHostList { type: 'serverReturnedHostList'; @@ -14,7 +14,7 @@ interface ServerReturnedHostList { interface ServerReturnedHostDetails { type: 'serverReturnedHostDetails'; - payload: HostMetadata; + payload: HostInfo; } interface ServerFailedToReturnHostDetails { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts index 9aff66cdfb75ef..6148934343635a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts @@ -52,7 +52,7 @@ describe('HostList store concerns', () => { }); const currentState = store.getState(); - expect(currentState.hosts).toEqual(payload.hosts); + expect(currentState.hosts).toEqual(payload.hosts.map(hostInfo => hostInfo.metadata)); expect(currentState.pageSize).toEqual(payload.request_page_size); expect(currentState.pageIndex).toEqual(payload.request_page_index); expect(currentState.total).toEqual(payload.total); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts index a1973a38b65347..8c8578426aa294 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -58,6 +58,6 @@ describe('host list middleware', () => { paging_properties: [{ page_index: 0 }, { page_size: 10 }], }), }); - expect(listData(getState())).toEqual(apiResponse.hosts); + expect(listData(getState())).toEqual(apiResponse.hosts.map(hostInfo => hostInfo.metadata)); }); }); 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 db39ecf4483122..d4c2602e34387e 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 } from '../../../../../common/types'; +import { HostResultList, HostStatus } from '../../../../../common/types'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; export const mockHostResultList: (options?: { @@ -27,7 +27,10 @@ export const mockHostResultList: (options?: { const hosts = []; for (let index = 0; index < actualCountToReturn; index++) { const generator = new EndpointDocGenerator('seed'); - hosts.push(generator.generateHostMetadata()); + hosts.push({ + metadata: generator.generateHostMetadata(), + host_status: HostStatus.ERROR, + }); } const mock: HostResultList = { hosts, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index fd70317a9f37ee..ad6741dab7be7f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -34,7 +34,7 @@ export const hostListReducer: Reducer = ( } = action.payload; return { ...state, - hosts, + hosts: hosts.map(hostInfo => hostInfo.metadata), total, pageSize, pageIndex, @@ -43,7 +43,7 @@ export const hostListReducer: Reducer = ( } else if (action.type === 'serverReturnedHostDetails') { return { ...state, - details: action.payload, + details: action.payload.metadata, }; } else if (action.type === 'serverFailedToReturnHostDetails') { return { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts index af9b49cea18adc..fb3e26157ef32a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts @@ -74,8 +74,12 @@ export const policyDetailsReducer: Reducer = ( ...state, location: action.payload, }; + const isCurrentlyOnDetailsPage = isOnPolicyDetailsPage(newState); + const wasPreviouslyOnDetailsPage = isOnPolicyDetailsPage(state); - if (isOnPolicyDetailsPage(newState)) { + // Did user just enter the Detail page? if so, then set the loading indicator and return new state + if (isCurrentlyOnDetailsPage && !wasPreviouslyOnDetailsPage) { + newState.isLoading = true; return newState; } return { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/app_root_provider.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/app_root_provider.tsx new file mode 100644 index 00000000000000..ca27831ee90b51 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/app_root_provider.tsx @@ -0,0 +1,58 @@ +/* + * 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, { memo, ReactNode, useMemo } from 'react'; +import { Provider } from 'react-redux'; +import { I18nProvider } from '@kbn/i18n/react'; +import { Router } from 'react-router-dom'; +import { History } from 'history'; +import { CoreStart } from 'kibana/public'; +import { useObservable } from 'react-use'; +import { EuiThemeProvider } from '../../../../../../legacy/common/eui_styled_components'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { appStoreFactory } from '../store'; +import { RouteCapture } from './route_capture'; +import { EndpointPluginStartDependencies } from '../../../plugin'; + +/** + * Provides the context for rendering the endpoint app + */ +export const AppRootProvider = memo<{ + store: ReturnType; + history: History; + coreStart: CoreStart; + depsStart: EndpointPluginStartDependencies; + children: ReactNode | ReactNode[]; +}>( + ({ + store, + history, + coreStart: { http, notifications, uiSettings, application }, + depsStart: { data }, + children, + }) => { + const isDarkMode = useObservable(uiSettings.get$('theme:darkMode')); + const services = useMemo(() => ({ http, notifications, application, data }), [ + application, + data, + http, + notifications, + ]); + return ( + + + + + + {children} + + + + + + ); + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/agents_summary.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/agents_summary.tsx index d0751cf9fb886e..a3e30eb891db43 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/agents_summary.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/agents_summary.tsx @@ -61,7 +61,7 @@ export const AgentsSummary = memo(props => { }, []); return ( - + {stats.map(({ key, title, health }) => { return ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx new file mode 100644 index 00000000000000..2ecc2b117bf017 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx @@ -0,0 +1,239 @@ +/* + * 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 from 'react'; +import { mount } from 'enzyme'; +import { createAppRootMockRenderer } from '../../mocks'; +import { PolicyDetails } from './policy_details'; +import { EndpointDocGenerator } from '../../../../../common/generate_data'; + +describe('Policy Details', () => { + type FindReactWrapperResponse = ReturnType['find']>; + + const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); + const generator = new EndpointDocGenerator(); + const { history, AppWrapper, coreStart } = createAppRootMockRenderer(); + const http = coreStart.http; + const render = (ui: Parameters[0]) => mount(ui, { wrappingComponent: AppWrapper }); + let policyDatasource: ReturnType; + let policyView: ReturnType; + + beforeEach(() => jest.clearAllMocks()); + + afterEach(() => { + if (policyView) { + policyView.unmount(); + } + }); + + describe('when displayed with invalid id', () => { + beforeEach(() => { + http.get.mockReturnValue(Promise.reject(new Error('policy not found'))); + history.push('/policy/1'); + policyView = render(); + }); + + it('should show loader followed by error message', () => { + expect(policyView.find('EuiLoadingSpinner').length).toBe(1); + policyView.update(); + const callout = policyView.find('EuiCallOut'); + expect(callout).toHaveLength(1); + expect(callout.prop('color')).toEqual('danger'); + expect(callout.text()).toEqual('policy not found'); + }); + }); + describe('when displayed with valid id', () => { + let asyncActions: Promise = Promise.resolve(); + + beforeEach(() => { + policyDatasource = generator.generatePolicyDatasource(); + policyDatasource.id = '1'; + + http.get.mockImplementation((...args) => { + const [path] = args; + if (typeof path === 'string') { + // GET datasouce + if (path === '/api/ingest_manager/datasources/1') { + asyncActions = asyncActions.then(async (): Promise => await sleep()); + return Promise.resolve({ + item: policyDatasource, + success: true, + }); + } + + // GET Agent status for agent config + if (path === '/api/ingest_manager/fleet/agent-status') { + asyncActions = asyncActions.then(async () => await sleep()); + return Promise.resolve({ + results: { events: 0, total: 5, online: 3, error: 1, offline: 1 }, + success: true, + }); + } + } + + return Promise.reject(new Error('unknown API call!')); + }); + history.push('/policy/1'); + policyView = render(); + }); + + it('should display back to list button and policy title', () => { + policyView.update(); + const pageHeaderLeft = policyView.find( + 'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"]' + ); + + const backToListButton = pageHeaderLeft.find('EuiButtonEmpty'); + expect(backToListButton.prop('iconType')).toBe('arrowLeft'); + expect(backToListButton.prop('href')).toBe('/mock/app/endpoint/policy'); + expect(backToListButton.text()).toBe('Back to policy list'); + + const pageTitle = pageHeaderLeft.find('[data-test-subj="pageViewHeaderLeftTitle"]'); + expect(pageTitle).toHaveLength(1); + expect(pageTitle.text()).toEqual(policyDatasource.name); + }); + it('should navigate to list if back to link is clicked', async () => { + policyView.update(); + const backToListButton = policyView.find( + 'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty' + ); + expect(history.location.pathname).toEqual('/policy/1'); + backToListButton.simulate('click'); + expect(history.location.pathname).toEqual('/policy'); + }); + it('should display agent stats', async () => { + await asyncActions; + policyView.update(); + const headerRight = policyView.find( + 'EuiPageHeaderSection[data-test-subj="pageViewHeaderRight"]' + ); + const agentsSummary = headerRight.find('EuiFlexGroup[data-test-subj="policyAgentsSummary"]'); + expect(agentsSummary).toHaveLength(1); + expect(agentsSummary.text()).toBe('Hosts5Online3Offline1Error1'); + }); + it('should display cancel button', async () => { + await asyncActions; + policyView.update(); + const cancelbutton = policyView.find( + 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' + ); + expect(cancelbutton).toHaveLength(1); + expect(cancelbutton.text()).toEqual('Cancel'); + }); + it('should redirect to policy list when cancel button is clicked', async () => { + await asyncActions; + policyView.update(); + const cancelbutton = policyView.find( + 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' + ); + expect(history.location.pathname).toEqual('/policy/1'); + cancelbutton.simulate('click'); + expect(history.location.pathname).toEqual('/policy'); + }); + it('should display save button', async () => { + await asyncActions; + policyView.update(); + const saveButton = policyView.find('EuiButton[data-test-subj="policyDetailsSaveButton"]'); + expect(saveButton).toHaveLength(1); + expect(saveButton.text()).toEqual('Save'); + }); + describe('when the save button is clicked', () => { + let saveButton: FindReactWrapperResponse; + let confirmModal: FindReactWrapperResponse; + let modalCancelButton: FindReactWrapperResponse; + let modalConfirmButton: FindReactWrapperResponse; + + beforeEach(async () => { + await asyncActions; + policyView.update(); + saveButton = policyView.find('EuiButton[data-test-subj="policyDetailsSaveButton"]'); + saveButton.simulate('click'); + policyView.update(); + confirmModal = policyView.find( + 'EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]' + ); + modalCancelButton = confirmModal.find('button[data-test-subj="confirmModalCancelButton"]'); + modalConfirmButton = confirmModal.find( + 'button[data-test-subj="confirmModalConfirmButton"]' + ); + http.put.mockImplementation((...args) => { + asyncActions = asyncActions.then(async () => await sleep()); + const [path] = args; + if (typeof path === 'string') { + if (path === '/api/ingest_manager/datasources/1') { + return Promise.resolve({ + item: policyDatasource, + success: true, + }); + } + } + + return Promise.reject(new Error('unknown PUT path!')); + }); + }); + + it('should show a modal confirmation', () => { + expect(confirmModal).toHaveLength(1); + expect(confirmModal.find('div[data-test-subj="confirmModalTitleText"]').text()).toEqual( + 'Save and deploy changes' + ); + expect(modalCancelButton.text()).toEqual('Cancel'); + expect(modalConfirmButton.text()).toEqual('Save and deploy changes'); + }); + it('should show info callout if policy is in use', () => { + const warningCallout = confirmModal.find( + 'EuiCallOut[data-test-subj="policyDetailsWarningCallout"]' + ); + expect(warningCallout).toHaveLength(1); + expect(warningCallout.text()).toEqual( + 'This action will update 5 hostsSaving these changes will apply the updates to all active endpoints assigned to this policy' + ); + }); + it('should close dialog if cancel button is clicked', () => { + modalCancelButton.simulate('click'); + expect( + policyView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]') + ).toHaveLength(0); + }); + it('should update policy and show success notification when confirm button is clicked', async () => { + modalConfirmButton.simulate('click'); + policyView.update(); + // Modal should be closed + expect( + policyView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]') + ).toHaveLength(0); + + // API should be called + await asyncActions; + expect(http.put.mock.calls[0][0]).toEqual(`/api/ingest_manager/datasources/1`); + policyView.update(); + + // Toast notification should be shown + const toastAddMock = coreStart.notifications.toasts.add.mock; + expect(toastAddMock.calls).toHaveLength(1); + expect(toastAddMock.calls[0][0]).toMatchObject({ + color: 'success', + iconType: 'check', + }); + }); + it('should show an error notification toast if update fails', async () => { + policyDatasource.id = 'invalid'; + modalConfirmButton.simulate('click'); + + await asyncActions; + policyView.update(); + + // Toast notification should be shown + const toastAddMock = coreStart.notifications.toasts.add.mock; + expect(toastAddMock.calls).toHaveLength(1); + expect(toastAddMock.calls[0][0]).toMatchObject({ + color: 'danger', + iconType: 'alert', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index 2dba301bf45376..bc56e5e6f63290 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -149,7 +149,10 @@ export const PolicyDetails = React.memo(() => { - + @@ -157,6 +160,7 @@ export const PolicyDetails = React.memo(() => { 0 && ( <> { +): Promise { const query = getESQueryHostMetadataByID(id); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', @@ -98,7 +98,7 @@ export async function getHostData( return undefined; } - return response.hits.hits[0]._source; + return enrichHostMetadata(response.hits.hits[0]._source); } function mapToHostResultList( @@ -113,7 +113,7 @@ function mapToHostResultList( hosts: searchResponse.hits.hits .map(response => response.inner_hits.most_recent.hits.hits) .flatMap(data => data as HitSource) - .map(entry => entry._source), + .map(entry => enrichHostMetadata(entry._source)), total: totalNumberOfHosts, }; } else { @@ -125,3 +125,10 @@ function mapToHostResultList( }; } } + +function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo { + return { + metadata: hostMetadata, + host_status: HostStatus.ERROR, + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index e0fd11e737e7dc..9bd251735cc044 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -18,7 +18,7 @@ import { httpServiceMock, loggingServiceMock, } from '../../../../../../src/core/server/mocks'; -import { HostMetadata, HostResultList } from '../../../common/types'; +import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { SearchResponse } from 'elasticsearch'; import { EndpointConfigSchema } from '../../config'; import * as data from '../../test_data/all_metadata_data.json'; @@ -230,7 +230,7 @@ describe('test endpoint route', () => { expect(message).toEqual('Endpoint Not Found'); }); - it('should return a single endpoint', async () => { + it('should return a single endpoint with status error', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: (data as any).hits.hits[0]._id }, }); @@ -257,8 +257,9 @@ describe('test endpoint route', () => { expect(mockScopedClient.callAsCurrentUser).toBeCalled(); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostMetadata; - expect(result).toHaveProperty('endpoint'); + const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; + expect(result).toHaveProperty('metadata.endpoint'); + expect(result.host_status).toEqual(HostStatus.ERROR); }); }); }); diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/feature.ts index 82fcc33f5c8ceb..ef32a8a80a0bd6 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 00000000000000..0186011382e84c --- /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 5b4f7728c9f311..2039f8f6acda2a 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 73a353cd274711..6140b7ac87ce02 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 fdeceb30b4e3d9..403d9586bf1600 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/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 da4b8e6f6eef24..95630a69818436 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 cd3ba43c3607c7..2e43ede2480ce5 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -17,6 +17,11 @@ import { import { IFieldType } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { + MetricExpressionParams, + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; import { euiStyled } from '../../../../../observability/public'; import { WhenExpression, @@ -36,16 +41,6 @@ import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explo import { useSource } from '../../../containers/source'; import { MetricsExplorerGroupBy } from '../../metrics_explorer/group_by'; -export interface MetricExpression { - aggType?: string; - metric?: string; - comparator?: Comparator; - threshold?: number[]; - timeSize?: number; - timeUnit?: TimeUnit; - indexPattern?: string; -} - interface AlertContextMeta { currentOptions?: Partial; series?: MetricsExplorerSeries; @@ -57,14 +52,35 @@ interface Props { criteria: MetricExpression[]; groupBy?: string; filterQuery?: string; + sourceId?: string; }; alertsContext: AlertsContextValue; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; } -type Comparator = '>' | '>=' | 'between' | '<' | '<='; type TimeUnit = 's' | 'm' | 'h' | 'd'; +type MetricExpression = Omit & { + metric?: string; +}; + +enum AGGREGATION_TYPES { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', + RATE = 'rate', + CARDINALITY = 'cardinality', +} + +const defaultExpression = { + aggType: AGGREGATION_TYPES.AVERAGE, + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', +} as MetricExpression; export const Expressions: React.FC = props => { const { setAlertParams, alertParams, errors, alertsContext } = props; @@ -87,18 +103,6 @@ export const Expressions: React.FC = props => { } }, [alertsContext.metadata]); - const defaultExpression = useMemo( - () => ({ - aggType: AGGREGATION_TYPES.AVERAGE, - comparator: '>', - threshold: [], - timeSize: 1, - timeUnit: 'm', - indexPattern: source?.configuration.metricAlias, - }), - [source] - ); - const updateParams = useCallback( (id, e: MetricExpression) => { const exp = alertParams.criteria ? alertParams.criteria.slice() : []; @@ -112,7 +116,7 @@ export const Expressions: React.FC = props => { const exp = alertParams.criteria.slice(); exp.push(defaultExpression); setAlertParams('criteria', exp); - }, [setAlertParams, alertParams.criteria, defaultExpression]); + }, [setAlertParams, alertParams.criteria]); const removeExpression = useCallback( (id: number) => { @@ -179,11 +183,10 @@ export const Expressions: React.FC = props => { 'criteria', md.currentOptions.metrics.map(metric => ({ metric: metric.field, - comparator: '>', + comparator: Comparator.GT, threshold: [], timeSize, timeUnit, - indexPattern: source?.configuration.metricAlias, aggType: metric.aggregation, })) ); @@ -201,6 +204,7 @@ export const Expressions: React.FC = props => { setAlertParams('groupBy', md.currentOptions.groupBy); } + setAlertParams('sourceId', source?.id); } }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps @@ -316,26 +320,31 @@ 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 => { const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props; - const { aggType = AGGREGATION_TYPES.MAX, metric, comparator = '>', threshold = [] } = expression; + const { + aggType = AGGREGATION_TYPES.MAX, + metric, + comparator = Comparator.GT, + threshold = [], + } = expression; const updateAggType = useCallback( (at: string) => { - setAlertParams(expressionId, { ...expression, aggType: at }); + setAlertParams(expressionId, { ...expression, aggType: at as MetricExpression['aggType'] }); }, [expressionId, expression, setAlertParams] ); const updateMetric = useCallback( - (m?: string) => { + (m?: MetricExpression['metric']) => { setAlertParams(expressionId, { ...expression, metric: m }); }, [expressionId, expression, setAlertParams] @@ -384,7 +393,7 @@ export const ExpressionRow: React.FC = props => { )} '} + thresholdComparator={comparator || Comparator.GT} threshold={threshold} onChangeSelectedThresholdComparator={updateComparator} onChangeSelectedThreshold={updateThreshold} @@ -411,16 +420,6 @@ export const ExpressionRow: React.FC = props => { ); }; -enum AGGREGATION_TYPES { - COUNT = 'count', - AVERAGE = 'avg', - SUM = 'sum', - MIN = 'min', - MAX = 'max', - RATE = 'rate', - CARDINALITY = 'cardinality', -} - export const aggregationType: { [key: string]: any } = { avg: { text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { 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 0f5b07f8c0e134..d84e46d08a2879 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx @@ -6,15 +6,14 @@ import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths - -import { MetricExpression } from './expression'; +import { MetricExpressionParams } from '../../../../server/lib/alerting/metric_threshold/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; export function validateMetricThreshold({ criteria, }: { - criteria: MetricExpression[]; + criteria: MetricExpressionParams[]; }): ValidationResult { const validationResult = { errors: {} }; const errors: { @@ -24,6 +23,7 @@ export function validateMetricThreshold({ timeWindowSize: string[]; threshold0: string[]; threshold1: string[]; + metric: string[]; }; } = {}; validationResult.errors = errors; @@ -42,6 +42,7 @@ export function validateMetricThreshold({ timeWindowSize: [], threshold0: [], threshold1: [], + metric: [], }; if (!c.aggType) { errors[id].aggField.push( @@ -74,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/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 09f17023495428..38cd0cec145f90 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -16,7 +16,8 @@ const executor = createMetricThresholdExecutor('test') as (opts: { const alertInstances = new Map(); const services = { - callCluster(_: string, { body }: any) { + callCluster(_: string, { body, index }: any) { + if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; const metric = body.query.bool.filter[1]?.exists.field; if (body.aggs.groupings) { if (body.aggs.groupings.composite.after) { @@ -55,6 +56,13 @@ const services = { }, }; }, + savedObjectsClient: { + get(_: string, sourceId: string) { + if (sourceId === 'alternate') + return { id: 'alternate', attributes: { metricAlias: 'alternatebeat-*' } }; + return { id: 'default', attributes: { metricAlias: 'metricbeat-*' } }; + }, + }, }; const baseCriterion = { @@ -62,15 +70,15 @@ const baseCriterion = { metric: 'test.metric.1', timeSize: 1, timeUnit: 'm', - indexPattern: 'metricbeat-*', }; describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = 'test-*'; - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ services, params: { + sourceId, criteria: [ { ...baseCriterion, @@ -134,6 +142,14 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction.action.thresholdOf.condition0).toStrictEqual([0.75]); expect(mostRecentAction.action.metricOf.condition0).toBe('test.metric.1'); }); + test('fetches the index pattern dynamically', async () => { + await execute(Comparator.LT, [17], 'alternate'); + expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); + expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + await execute(Comparator.LT, [1.5], 'alternate'); + expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); + expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + }); }); describe('querying with a groupBy parameter', () => { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 60bba61b75ef14..c5ea65f7a4d1ad 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -5,6 +5,8 @@ */ import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { convertSavedObjectToSavedSourceConfiguration } from '../../sources/sources'; +import { infraSourceConfigurationSavedObjectType } from '../../sources/saved_object_mappings'; import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; @@ -15,6 +17,7 @@ import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; const TOTAL_BUCKETS = 5; +const DEFAULT_INDEX_PATTERN = 'metricbeat-*'; interface Aggregation { aggregatedIntervals: { @@ -165,18 +168,42 @@ export const getElasticsearchMetricQuery = ( }; }; +const getIndexPattern: ( + services: AlertServices, + sourceId?: string +) => Promise = async function({ savedObjectsClient }, sourceId = 'default') { + try { + const sourceConfiguration = await savedObjectsClient.get( + infraSourceConfigurationSavedObjectType, + sourceId + ); + const { metricAlias } = convertSavedObjectToSavedSourceConfiguration( + sourceConfiguration + ).configuration; + return metricAlias || DEFAULT_INDEX_PATTERN; + } catch (e) { + if (e.output.statusCode === 404) { + return DEFAULT_INDEX_PATTERN; + } else { + throw e; + } + } +}; + const getMetric: ( services: AlertServices, params: MetricExpressionParams, + index: string, groupBy: string | undefined, filterQuery: string | undefined ) => Promise> = async function( - { callCluster }, + { savedObjectsClient, callCluster }, params, + index, groupBy, filterQuery ) { - const { indexPattern, aggType } = params; + const { aggType } = params; const searchBody = getElasticsearchMetricQuery(params, groupBy, filterQuery); try { @@ -189,7 +216,7 @@ const getMetric: ( response => response.aggregations?.groupings?.after_key ); const compositeBuckets = (await getAllCompositeData( - body => callCluster('search', { body, index: indexPattern }), + body => callCluster('search', { body, index }), searchBody, bucketSelector, afterKeyHandler @@ -204,7 +231,7 @@ const getMetric: ( } const result = await callCluster('search', { body: searchBody, - index: indexPattern, + index, }); return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) }; } catch (e) { @@ -236,16 +263,18 @@ const mapToConditionsLookup = ( export const createMetricThresholdExecutor = (alertUUID: string) => async function({ services, params }: AlertExecutorOptions) { - const { criteria, groupBy, filterQuery } = params as { + const { criteria, groupBy, filterQuery, sourceId } = params as { criteria: MetricExpressionParams[]; groupBy: string | undefined; filterQuery: string | undefined; + sourceId?: string; }; const alertResults = await Promise.all( criteria.map(criterion => (async () => { - const currentValues = await getMetric(services, criterion, groupBy, filterQuery); + const index = await getIndexPattern(services, sourceId); + const currentValues = await getMetric(services, criterion, index, groupBy, filterQuery); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 2ab3a3322661d1..8808219cabaa70 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -29,7 +29,6 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet ]), timeUnit: schema.string(), timeSize: schema.number(), - indexPattern: schema.string(), }; const nonCountCriterion = schema.object({ @@ -89,6 +88,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])), groupBy: schema.maybe(schema.string()), filterQuery: schema.maybe(schema.string()), + sourceId: schema.string(), }), }, defaultActionGroupId: FIRED_ACTIONS.id, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index e87ffcfb2b912d..66e0a363c89833 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -26,6 +26,17 @@ const bucketsB = [ }, ]; +const bucketsC = [ + { + doc_count: 2, + aggregatedValue: { value: 0.5 }, + }, + { + doc_count: 3, + aggregatedValue: { value: 16.0 }, + }, +]; + export const basicMetricResponse = { aggregations: { aggregatedIntervals: { @@ -108,3 +119,11 @@ export const compositeEndResponse = { aggregations: {}, hits: { total: { value: 0 } }, }; + +export const changedSourceIdResponse = { + aggregations: { + aggregatedIntervals: { + buckets: bucketsC, + }, + }, +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index 557a071ec91752..abed691f109c00 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -28,7 +28,7 @@ export type TimeUnit = 's' | 'm' | 'h' | 'd'; interface BaseMetricExpressionParams { timeSize: number; timeUnit: TimeUnit; - indexPattern: string; + sourceId?: string; threshold: number[]; comparator: Comparator; } diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 5b9207a88e6e47..c7ff6c96382043 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -231,7 +231,7 @@ const mergeSourceConfiguration = ( first ); -const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknown) => +export const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknown) => pipe( SourceConfigurationSavedObjectRuntimeType.decode(savedObject), map(savedSourceConfiguration => ({ 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 ab039be8e7c22d..b897c03e89f821 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 f58eaacb7be673..20bbbec8919d63 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/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 356739af1ff9a8..0e8763cb2d4c09 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 74b08f48df12de..6b0c68ccb7d3fa 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 3bf5b2bb4c0f02..43e8f5a2c060db 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 bcb99eed88ac04..846a807f9240d3 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 e5f18e1449d1b2..3bfca756689115 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 23d0f3317a6673..7815ab9cd1d6ed 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 44e5bfa41cb9bf..d99f0712db3c36 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 00000000000000..a45fabeb5ed6a3 --- /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 00000000000000..518e2bfc1af07a --- /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 b45beef4a8b5e4..105d6c66a57047 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 0aa08602e4d4de..5ebd1300baf65d 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 333a9b049fa853..32615278b67d76 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/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index a941494072ae3d..309ddca3784c29 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -319,9 +319,9 @@ class AgentConfigService { return outputs; }, {} as FullAgentConfig['outputs']), }, - datasources: (config.datasources as Datasource[]).map(ds => - storedDatasourceToAgentDatasource(ds) - ), + datasources: (config.datasources as Datasource[]) + .filter(datasource => datasource.enabled) + .map(ds => storedDatasourceToAgentDatasource(ds)), revision: config.revision, }; diff --git a/x-pack/plugins/maps/public/components/metric_editor.js b/x-pack/plugins/maps/public/components/metric_editor.js index 530f402592b2ba..d1affe2f42190f 100644 --- a/x-pack/plugins/maps/public/components/metric_editor.js +++ b/x-pack/plugins/maps/public/components/metric_editor.js @@ -24,8 +24,13 @@ function filterFieldsForAgg(fields, aggType) { return getTermsFields(fields); } + const metricAggFieldTypes = ['number']; + if (aggType !== AGG_TYPE.SUM) { + metricAggFieldTypes.push('date'); + } + return fields.filter(field => { - return field.aggregatable && field.type === 'number'; + return field.aggregatable && metricAggFieldTypes.includes(field.type); }); } 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 65f952ca01038c..34f7dd4b9578fc 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 24011c51ddbaad..acd05475f97627 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/ml/common/constants/file_datavisualizer.ts b/x-pack/plugins/ml/common/constants/file_datavisualizer.ts index d72e4d63cc47e8..81d51bfa258160 100644 --- a/x-pack/plugins/ml/common/constants/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/constants/file_datavisualizer.ts @@ -5,6 +5,8 @@ */ export const MAX_BYTES = 104857600; +export const ABSOLUTE_MAX_BYTES = MAX_BYTES * 5; +export const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; // Value to use in the Elasticsearch index mapping meta data to identify the // index as having been created by the ML File Data Visualizer. diff --git a/x-pack/plugins/ml/common/types/ml_config.ts b/x-pack/plugins/ml/common/types/ml_config.ts new file mode 100644 index 00000000000000..8fd9fd22bad8af --- /dev/null +++ b/x-pack/plugins/ml/common/types/ml_config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { MAX_BYTES } from '../constants/file_datavisualizer'; + +export const configSchema = schema.object({ + file_data_visualizer: schema.object({ + max_file_size_bytes: schema.number({ defaultValue: MAX_BYTES }), + }), +}); + +export type MlConfigType = TypeOf; diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index e9796fcbb0fe44..f1facd18b9da5b 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -15,10 +15,14 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p import { setDependencyCache, clearCache } from './util/dependency_cache'; import { setLicenseCache } from './license'; import { MlSetupDependencies, MlStartDependencies } from '../plugin'; +import { MlConfigType } from '../../common/types/ml_config'; import { MlRouter } from './routing'; -type MlDependencies = MlSetupDependencies & MlStartDependencies; +type MlDependencies = MlSetupDependencies & + MlStartDependencies & { + mlConfig: MlConfigType; + }; interface AppProps { coreStart: CoreStart; @@ -74,6 +78,7 @@ export const renderApp = ( http: coreStart.http, security: deps.security, urlGenerators: deps.share.urlGenerators, + mlConfig: deps.mlConfig, }); const mlLicense = setLicenseCache(deps.licensing); 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 e5f30a50ed8f0d..0c83dfb6a2346e 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 @@ -250,7 +250,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; 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 9c0bc69f4b41f9..4bd03fec7cc729 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 01a39d2ef9f3b6..e121268e65e86c 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/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index a8bb5a0a8fe102..2d6505f5ce1f75 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -26,6 +26,7 @@ import { isFullLicense } from '../license'; import { useTimefilter, useMlKibana } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; +import { getMaxBytesFormatted } from './file_based/components/utils'; function startTrialDescription() { return ( @@ -59,6 +60,8 @@ export const DatavisualizerSelector: FC = () => { licenseManagement.enabled === true && isFullLicense() === false; + const maxFileSize = getMaxBytesFormatted(); + return ( @@ -102,7 +105,8 @@ export const DatavisualizerSelector: FC = () => { description={ } betaBadgeLabel={i18n.translate( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.tsx index c56ab021e2f2f6..49b2396aeca13e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { ExperimentalBadge } from '../experimental_badge'; +import { getMaxBytesFormatted } from '../utils'; export const WelcomeContent: FC = () => { const toolTipContent = i18n.translate( @@ -28,6 +29,8 @@ export const WelcomeContent: FC = () => { } ); + const maxFileSize = getMaxBytesFormatted(); + return ( @@ -117,7 +120,8 @@ export const WelcomeContent: FC = () => {

diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index 02f14a9e4553e5..d1b615a878b2b5 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -19,14 +19,15 @@ import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts'; import { EditFlyout } from '../edit_flyout'; import { ExplanationFlyout } from '../explanation_flyout'; import { ImportView } from '../import_view'; -import { MAX_BYTES } from '../../../../../../common/constants/file_datavisualizer'; import { + getMaxBytes, readFile, createUrlOverrides, processResults, reduceData, hasImportPermission, } from '../utils'; + import { MODE } from './constants'; const UPLOAD_SIZE_MB = 5; @@ -57,6 +58,7 @@ export class FileDataVisualizerView extends Component { this.overrides = {}; this.previousOverrides = {}; this.originalSettings = {}; + this.maxFileUploadBytes = getMaxBytes(); } async componentDidMount() { @@ -93,7 +95,7 @@ export class FileDataVisualizerView extends Component { }; async loadFile(file) { - if (file.size <= MAX_BYTES) { + if (file.size <= this.maxFileUploadBytes) { try { const fileContents = await readFile(file); const data = fileContents.data; @@ -105,7 +107,6 @@ export class FileDataVisualizerView extends Component { await this.loadSettings(data); } catch (error) { - console.error(error); this.setState({ loaded: false, loading: false, @@ -181,8 +182,6 @@ export class FileDataVisualizerView extends Component { fileCouldNotBeRead: isRetry, }); } catch (error) { - console.error(error); - this.setState({ results: undefined, explanation: undefined, @@ -287,7 +286,9 @@ export class FileDataVisualizerView extends Component { {loading && } - {fileTooLarge && } + {fileTooLarge && ( + + )} {fileCouldNotBeRead && loading === false && ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx index 3dbc20f16012b5..7333c96a0d8ead 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx @@ -11,8 +11,7 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { ErrorResponse } from '../../../../../../common/types/errors'; - -const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; +import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../../common/constants/file_datavisualizer'; interface FileTooLargeProps { fileSize: number; @@ -81,7 +80,7 @@ interface FileCouldNotBeReadProps { } export const FileCouldNotBeRead: FC = ({ error, loaded }) => { - const message = error.body.message; + const message = error?.body?.message || ''; return ( = ({ error, loaded }; export const Explanation: FC<{ error: ErrorResponse }> = ({ error }) => { - if (!error.body.attributes?.body?.error?.suppressed?.length) { + if (!error?.body?.attributes?.body?.error?.suppressed?.length) { return null; } const reason: string = error.body.attributes.body.error.suppressed[0].reason; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts index 6f670359586b7c..0f0036a7c46164 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts @@ -10,4 +10,6 @@ export { processResults, readFile, reduceData, + getMaxBytes, + getMaxBytesFormatted, } from './utils'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 5048065ae60fa8..0d2016b71ed837 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -5,8 +5,14 @@ */ import { isEqual } from 'lodash'; +import numeral from '@elastic/numeral'; import { ml } from '../../../../services/ml_api_service'; import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer'; +import { + ABSOLUTE_MAX_BYTES, + FILE_SIZE_DISPLAY_FORMAT, +} from '../../../../../../common/constants/file_datavisualizer'; +import { getMlConfig } from '../../../../util/dependency_cache'; const DEFAULT_LINES_TO_SAMPLE = 1000; @@ -54,6 +60,15 @@ export function reduceData(data: string, mb: number) { return data.length >= size ? data.slice(0, size) : data; } +export function getMaxBytes() { + const maxBytes = getMlConfig().file_data_visualizer.max_file_size_bytes; + return maxBytes < ABSOLUTE_MAX_BYTES ? maxBytes : ABSOLUTE_MAX_BYTES; +} + +export function getMaxBytesFormatted() { + return numeral(getMaxBytes()).format(FILE_SIZE_DISPLAY_FORMAT); +} + export function createUrlOverrides(overrides: InputOverrides, originalSettings: InputOverrides) { const formattedOverrides: InputOverrides = {}; for (const o in overrideDefaults) { diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index d5605d3bca65f5..934a0a5e9ae3af 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -23,6 +23,7 @@ import { } from 'kibana/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { SecurityPluginSetup } from '../../../../security/public'; +import { MlConfigType } from '../../../common/types/ml_config'; export interface DependencyCache { timefilter: DataPublicPluginSetup['query']['timefilter'] | null; @@ -42,6 +43,7 @@ export interface DependencyCache { security: SecurityPluginSetup | null; i18n: I18nStart | null; urlGenerators: SharePluginStart['urlGenerators'] | null; + mlConfig: MlConfigType | null; } const cache: DependencyCache = { @@ -62,6 +64,7 @@ const cache: DependencyCache = { security: null, i18n: null, urlGenerators: null, + mlConfig: null, }; export function setDependencyCache(deps: Partial) { @@ -82,6 +85,7 @@ export function setDependencyCache(deps: Partial) { cache.security = deps.security || null; cache.i18n = deps.i18n || null; cache.urlGenerators = deps.urlGenerators || null; + cache.mlConfig = deps.mlConfig || null; } export function getTimefilter() { @@ -202,6 +206,13 @@ export function getGetUrlGenerator() { return cache.urlGenerators.getUrlGenerator; } +export function getMlConfig() { + if (cache.mlConfig === null) { + throw new Error("mlConfig hasn't been initialized"); + } + return cache.mlConfig; +} + export function clearCache() { console.log('clearing dependency cache'); // eslint-disable-line no-console Object.keys(cache).forEach(k => { diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 8070f94a1264dd..4697496270edf2 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializer } from 'kibana/public'; +import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; import './index.scss'; import { MlPlugin, @@ -19,6 +19,6 @@ export const plugin: PluginInitializer< MlPluginStart, MlSetupDependencies, MlStartDependencies -> = () => new MlPlugin(); +> = (context: PluginInitializerContext) => new MlPlugin(context); export { MlPluginSetup, MlPluginStart }; diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 2472c343d05104..b51be4d2486839 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -5,7 +5,13 @@ */ import { i18n } from '@kbn/i18n'; -import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'; +import { + Plugin, + CoreStart, + CoreSetup, + AppMountParameters, + PluginInitializerContext, +} from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -19,6 +25,7 @@ import { LicenseManagementUIPluginSetup } from '../../license_management/public' import { setDependencyCache } from './application/util/dependency_cache'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; import { registerFeature } from './register_feature'; +import { MlConfigType } from '../common/types/ml_config'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -34,7 +41,10 @@ export interface MlSetupDependencies { } export class MlPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + setup(core: CoreSetup, pluginsSetup: MlSetupDependencies) { + const mlConfig = this.initializerContext.config.get(); core.application.register({ id: PLUGIN_ID, title: i18n.translate('xpack.ml.plugin.title', { @@ -57,6 +67,7 @@ export class MlPlugin implements Plugin { usageCollection: pluginsSetup.usageCollection, licenseManagement: pluginsSetup.licenseManagement, home: pluginsSetup.home, + mlConfig, }, { element: params.element, diff --git a/x-pack/plugins/ml/server/config.ts b/x-pack/plugins/ml/server/config.ts new file mode 100644 index 00000000000000..7cef6f17bbefb4 --- /dev/null +++ b/x-pack/plugins/ml/server/config.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 { PluginConfigDescriptor } from 'kibana/server'; +import { MlConfigType, configSchema } from '../common/types/ml_config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + file_data_visualizer: true, + }, + schema: configSchema, +}; diff --git a/x-pack/plugins/ml/server/index.ts b/x-pack/plugins/ml/server/index.ts index 175c20bf49c947..6e638d647a3875 100644 --- a/x-pack/plugins/ml/server/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -9,3 +9,5 @@ import { MlServerPlugin } from './plugin'; export { MlPluginSetup, MlPluginStart } from './plugin'; export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); + +export { config } from './config'; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 7d3ef116e67ab7..c7add12be142cb 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 ({ + executeActions: jest.fn(), + getUiMessage: jest.fn(), +})); + +jest.mock('../lib/alerts/get_prepared_alert', () => ({ + getPreparedAlert: jest.fn(() => { + return { + emailAddress: 'foo@foo.com', + }; + }), +})); + +interface MockServices { + callCluster: jest.Mock; + alertInstanceFactory: jest.Mock; + savedObjectsClient: jest.Mock; +} + +describe('getClusterState', () => { + const services: MockServices | AlertServices = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + + const params: AlertCommonParams = { + dateFormat: 'YYYY', + timezone: 'UTC', + }; + + const emailAddress = 'foo@foo.com'; + const clusterUuid = 'kdksdfj434'; + const clusterName = 'monitoring_test'; + const cluster = { clusterUuid, clusterName }; + + async function setupAlert( + previousState: AlertClusterStateState, + newState: AlertClusterStateState + ): Promise { + const logger: Logger = { + warn: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + info: jest.fn(), + get: jest.fn(), + }; + const getLogger = (): Logger => logger; + const ccrEnabled = false; + (getPreparedAlert as jest.Mock).mockImplementation(() => ({ + emailAddress, + data: [ + { + state: newState, + clusterUuid, + }, + ], + clusters: [cluster], + })); + + const alert = getClusterState(null as any, null as any, getLogger, ccrEnabled); + const state: AlertCommonState = { + [clusterUuid]: { + state: previousState, + ui: { + isFiring: false, + severity: 0, + message: null, + resolvedMS: 0, + lastCheckedMS: 0, + triggeredMS: 0, + }, + } as AlertClusterStatePerClusterState, + }; + + return (await alert.executor({ services, params, state } as any)) as AlertCommonState; + } + + afterEach(() => { + (executeActions as jest.Mock).mockClear(); + }); + + it('should configure the alert properly', () => { + const alert = getClusterState(null as any, null as any, jest.fn(), false); + expect(alert.id).toBe(ALERT_TYPE_CLUSTER_STATE); + expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); + }); + + it('should alert if green -> yellow', async () => { + const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Yellow); + expect(executeActions).toHaveBeenCalledWith( + undefined, + cluster, + AlertClusterStateState.Yellow, + emailAddress + ); + const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; + expect(clusterResult.state).toBe(AlertClusterStateState.Yellow); + expect(clusterResult.ui.isFiring).toBe(true); + expect(clusterResult.ui.resolvedMS).toBe(0); + }); + + it('should alert if yellow -> green', async () => { + const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Green); + expect(executeActions).toHaveBeenCalledWith( + undefined, + cluster, + AlertClusterStateState.Green, + emailAddress, + true + ); + const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; + expect(clusterResult.state).toBe(AlertClusterStateState.Green); + expect(clusterResult.ui.resolvedMS).toBeGreaterThan(0); + }); + + it('should alert if green -> red', async () => { + const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Red); + expect(executeActions).toHaveBeenCalledWith( + undefined, + cluster, + AlertClusterStateState.Red, + emailAddress + ); + const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; + expect(clusterResult.state).toBe(AlertClusterStateState.Red); + expect(clusterResult.ui.isFiring).toBe(true); + expect(clusterResult.ui.resolvedMS).toBe(0); + }); + + it('should alert if red -> green', async () => { + const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Green); + expect(executeActions).toHaveBeenCalledWith( + undefined, + cluster, + AlertClusterStateState.Green, + emailAddress, + true + ); + const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; + expect(clusterResult.state).toBe(AlertClusterStateState.Green); + expect(clusterResult.ui.resolvedMS).toBeGreaterThan(0); + }); + + it('should not alert if red -> yellow', async () => { + const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Yellow); + expect(executeActions).not.toHaveBeenCalled(); + const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; + expect(clusterResult.state).toBe(AlertClusterStateState.Red); + expect(clusterResult.ui.resolvedMS).toBe(0); + }); + + it('should not alert if yellow -> red', async () => { + const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Red); + expect(executeActions).not.toHaveBeenCalled(); + const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; + expect(clusterResult.state).toBe(AlertClusterStateState.Yellow); + expect(clusterResult.ui.resolvedMS).toBe(0); + }); + + it('should not alert if green -> green', async () => { + const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Green); + expect(executeActions).not.toHaveBeenCalled(); + const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; + expect(clusterResult.state).toBe(AlertClusterStateState.Green); + expect(clusterResult.ui.resolvedMS).toBe(0); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts new file mode 100644 index 00000000000000..9a5805b8af7ced --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts @@ -0,0 +1,134 @@ +/* + * 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 moment from 'moment-timezone'; +import { i18n } from '@kbn/i18n'; +import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; +import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants'; +import { AlertType } from '../../../alerting/server'; +import { executeActions, getUiMessage } from '../lib/alerts/cluster_state.lib'; +import { + AlertCommonExecutorOptions, + AlertCommonState, + AlertClusterStatePerClusterState, + AlertCommonCluster, +} from './types'; +import { AlertClusterStateState } from './enums'; +import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; +import { fetchClusterState } from '../lib/alerts/fetch_cluster_state'; + +export const getClusterState = ( + getUiSettingsService: () => Promise, + monitoringCluster: ICustomClusterClient, + getLogger: (...scopes: string[]) => Logger, + ccsEnabled: boolean +): AlertType => { + const logger = getLogger(ALERT_TYPE_CLUSTER_STATE); + return { + id: ALERT_TYPE_CLUSTER_STATE, + name: 'Monitoring Alert - Cluster Status', + actionGroups: [ + { + id: 'default', + name: i18n.translate('xpack.monitoring.alerts.clusterState.actionGroups.default', { + defaultMessage: 'Default', + }), + }, + ], + defaultActionGroupId: 'default', + async executor({ + services, + params, + state, + }: AlertCommonExecutorOptions): Promise { + logger.debug( + `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` + ); + + const preparedAlert = await getPreparedAlert( + ALERT_TYPE_CLUSTER_STATE, + getUiSettingsService, + monitoringCluster, + logger, + ccsEnabled, + services, + fetchClusterState + ); + + if (!preparedAlert) { + return state; + } + + const { emailAddress, data: states, clusters } = preparedAlert; + + const result: AlertCommonState = { ...state }; + const defaultAlertState: AlertClusterStatePerClusterState = { + state: AlertClusterStateState.Green, + ui: { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + triggeredMS: 0, + lastCheckedMS: 0, + }, + }; + + for (const clusterState of states) { + const alertState: AlertClusterStatePerClusterState = + (state[clusterState.clusterUuid] as AlertClusterStatePerClusterState) || + defaultAlertState; + const cluster = clusters.find( + (c: AlertCommonCluster) => c.clusterUuid === clusterState.clusterUuid + ); + if (!cluster) { + logger.warn(`Unable to find cluster for clusterUuid='${clusterState.clusterUuid}'`); + continue; + } + const isNonGreen = clusterState.state !== AlertClusterStateState.Green; + const severity = clusterState.state === AlertClusterStateState.Red ? 2100 : 1100; + + const ui = alertState.ui; + let triggered = ui.triggeredMS; + let resolved = ui.resolvedMS; + let message = ui.message || {}; + let lastState = alertState.state; + const instance = services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE); + + if (isNonGreen) { + if (lastState === AlertClusterStateState.Green) { + logger.debug(`Cluster state changed from green to ${clusterState.state}`); + executeActions(instance, cluster, clusterState.state, emailAddress); + lastState = clusterState.state; + triggered = moment().valueOf(); + } + message = getUiMessage(clusterState.state); + resolved = 0; + } else if (!isNonGreen && lastState !== AlertClusterStateState.Green) { + logger.debug(`Cluster state changed from ${lastState} to green`); + executeActions(instance, cluster, clusterState.state, emailAddress, true); + lastState = clusterState.state; + message = getUiMessage(clusterState.state, true); + resolved = moment().valueOf(); + } + + result[clusterState.clusterUuid] = { + state: lastState, + ui: { + message, + isFiring: isNonGreen, + severity, + resolvedMS: resolved, + triggeredMS: triggered, + lastCheckedMS: moment().valueOf(), + }, + } as AlertClusterStatePerClusterState; + } + + return result; + }, + }; +}; diff --git a/x-pack/plugins/monitoring/server/alerts/enums.ts b/x-pack/plugins/monitoring/server/alerts/enums.ts new file mode 100644 index 00000000000000..ccff588743af1b --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/enums.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum AlertClusterStateState { + Green = 'green', + Red = 'red', + Yellow = 'yellow', +} + +export enum AlertCommonPerClusterMessageTokenType { + Time = 'time', + Link = 'link', +} diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts index 0773af6e7f070d..92047e300bc1ff 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -6,42 +6,31 @@ import moment from 'moment-timezone'; import { getLicenseExpiration } from './license_expiration'; -import { - ALERT_TYPE_LICENSE_EXPIRATION, - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, -} from '../../common/constants'; +import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; import { Logger } from 'src/core/server'; -import { AlertServices, AlertInstance } from '../../../alerting/server'; +import { AlertServices } from '../../../alerting/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { - AlertState, - AlertClusterState, - AlertParams, - LicenseExpirationAlertExecutorOptions, + AlertCommonParams, + AlertCommonState, + AlertLicensePerClusterState, + AlertLicense, } from './types'; -import { SavedObject, SavedObjectAttributes } from 'src/core/server'; -import { SavedObjectsClientContract } from 'src/core/server'; - -function fillLicense(license: any, clusterUuid?: string) { - return { - hits: { - hits: [ - { - _source: { - license, - cluster_uuid: clusterUuid, - }, - }, - ], - }, - }; -} - -const clusterUuid = 'a4545jhjb'; -const params: AlertParams = { - dateFormat: 'YYYY', - timezone: 'UTC', -}; +import { executeActions } from '../lib/alerts/license_expiration.lib'; +import { PreparedAlert, getPreparedAlert } from '../lib/alerts/get_prepared_alert'; + +jest.mock('../lib/alerts/license_expiration.lib', () => ({ + executeActions: jest.fn(), + getUiMessage: jest.fn(), +})); + +jest.mock('../lib/alerts/get_prepared_alert', () => ({ + getPreparedAlert: jest.fn(() => { + return { + emailAddress: 'foo@foo.com', + }; + }), +})); interface MockServices { callCluster: jest.Mock; @@ -49,428 +38,169 @@ interface MockServices { savedObjectsClient: jest.Mock; } -const alertExecutorOptions: LicenseExpirationAlertExecutorOptions = { - alertId: '', - startedAt: new Date(), - services: { - callCluster: (path: string, opts: any) => new Promise(resolve => resolve()), - alertInstanceFactory: (id: string) => new AlertInstance(), - savedObjectsClient: {} as jest.Mocked, - }, - params: {}, - state: {}, - spaceId: '', - name: '', - tags: [], - previousStartedAt: null, - createdBy: null, - updatedBy: null, -}; - describe('getLicenseExpiration', () => { - const emailAddress = 'foo@foo.com'; - const getUiSettingsService: any = () => ({ - asScopedToClient: (): any => ({ - get: () => new Promise(resolve => resolve(emailAddress)), - }), - }); - const monitoringCluster: any = null; - const logger: Logger = { - warn: jest.fn(), - log: jest.fn(), - debug: jest.fn(), - trace: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), - info: jest.fn(), - get: jest.fn(), + const services: MockServices | AlertServices = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), }; - const getLogger = (): Logger => logger; - const ccrEnabled = false; - afterEach(() => { - (logger.warn as jest.Mock).mockClear(); - }); - - it('should have the right id and actionGroups', () => { - const alert = getLicenseExpiration( - getUiSettingsService, - monitoringCluster, - getLogger, - ccrEnabled - ); - expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); - expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); - }); + const params: AlertCommonParams = { + dateFormat: 'YYYY', + timezone: 'UTC', + }; - it('should return the state if no license is provided', async () => { - const alert = getLicenseExpiration( - getUiSettingsService, - monitoringCluster, - getLogger, - ccrEnabled - ); + const emailAddress = 'foo@foo.com'; + const clusterUuid = 'kdksdfj434'; + const clusterName = 'monitoring_test'; + const dateFormat = 'YYYY-MM-DD'; + const cluster = { clusterUuid, clusterName }; + const defaultUiState = { + isFiring: false, + severity: 0, + message: null, + resolvedMS: 0, + lastCheckedMS: 0, + triggeredMS: 0, + }; - const services: MockServices | AlertServices = { - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), + async function setupAlert( + license: AlertLicense | null, + expiredCheckDateMS: number, + preparedAlertResponse: PreparedAlert | null | undefined = undefined + ): Promise { + const logger: Logger = { + warn: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + info: jest.fn(), + get: jest.fn(), }; - const state = { foo: 1 }; - - const result = await alert.executor({ - ...alertExecutorOptions, - services, - params, - state, - }); - - expect(result).toEqual(state); - }); + const getLogger = (): Logger => logger; + const ccrEnabled = false; + (getPreparedAlert as jest.Mock).mockImplementation(() => { + if (preparedAlertResponse !== undefined) { + return preparedAlertResponse; + } - it('should log a warning if no email is provided', async () => { - const customGetUiSettingsService: any = () => ({ - asScopedToClient: () => ({ - get: () => null, - }), + return { + emailAddress, + data: [license], + clusters: [cluster], + dateFormat, + }; }); - const alert = getLicenseExpiration( - customGetUiSettingsService, - monitoringCluster, - getLogger, - ccrEnabled - ); - const services = { - callCluster: jest.fn( - (method: string, { filterPath }): Promise => { - return new Promise(resolve => { - if (filterPath.includes('hits.hits._source.license.*')) { - resolve( - fillLicense({ - status: 'good', - type: 'basic', - expiry_date_in_millis: moment() - .add(7, 'days') - .valueOf(), - }) - ); - } - resolve({}); - }); - } - ), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), + const alert = getLicenseExpiration(null as any, null as any, getLogger, ccrEnabled); + const state: AlertCommonState = { + [clusterUuid]: { + expiredCheckDateMS, + ui: { ...defaultUiState }, + } as AlertLicensePerClusterState, }; - const state = {}; + return (await alert.executor({ services, params, state } as any)) as AlertCommonState; + } - await alert.executor({ - ...alertExecutorOptions, - services, - params, - state, - }); - - expect((logger.warn as jest.Mock).mock.calls.length).toBe(1); - expect(logger.warn).toHaveBeenCalledWith( - `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` - ); + afterEach(() => { + (executeActions as jest.Mock).mockClear(); + (getPreparedAlert as jest.Mock).mockClear(); }); - it('should fire actions if going to expire', async () => { - const scheduleActions = jest.fn(); - const alertInstanceFactory = jest.fn( - (id: string): AlertInstance => { - const instance = new AlertInstance(); - instance.scheduleActions = scheduleActions; - return instance; - } - ); + it('should have the right id and actionGroups', () => { + const alert = getLicenseExpiration(null as any, null as any, jest.fn(), false); + expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); + expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); + }); - const alert = getLicenseExpiration( - getUiSettingsService, - monitoringCluster, - getLogger, - ccrEnabled - ); + it('should return the state if no license is provided', async () => { + const result = await setupAlert(null, 0, null); + expect(result[clusterUuid].ui).toEqual(defaultUiState); + }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockReturnValue( - new Promise(resolve => { - const savedObject: SavedObject = { - id: '', - type: '', - references: [], - attributes: { - [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, - }, - }; - resolve(savedObject); - }) - ); - const services = { - callCluster: jest.fn( - (method: string, { filterPath }): Promise => { - return new Promise(resolve => { - if (filterPath.includes('hits.hits._source.license.*')) { - resolve( - fillLicense( - { - status: 'active', - type: 'gold', - expiry_date_in_millis: moment() - .add(7, 'days') - .valueOf(), - }, - clusterUuid - ) - ); - } - resolve({}); - }); - } - ), - alertInstanceFactory, - savedObjectsClient, + it('should fire actions if going to expire', async () => { + const expiryDateMS = moment() + .add(7, 'days') + .valueOf(); + const license = { + status: 'active', + type: 'gold', + expiryDateMS, + clusterUuid, }; - - const state = {}; - - const result: AlertState = (await alert.executor({ - ...alertExecutorOptions, - services, - params, - state, - })) as AlertState; - - const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; - + const result = await setupAlert(license, 0); + const newState = result[clusterUuid] as AlertLicensePerClusterState; expect(newState.expiredCheckDateMS > 0).toBe(true); - expect(scheduleActions.mock.calls.length).toBe(1); - expect(scheduleActions.mock.calls[0][1].subject).toBe( - 'NEW X-Pack Monitoring: License Expiration' + expect(executeActions).toHaveBeenCalledWith( + undefined, + cluster, + moment.utc(expiryDateMS), + dateFormat, + emailAddress ); - expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); }); it('should fire actions if the user fixed their license', async () => { - const scheduleActions = jest.fn(); - const alertInstanceFactory = jest.fn( - (id: string): AlertInstance => { - const instance = new AlertInstance(); - instance.scheduleActions = scheduleActions; - return instance; - } - ); - const alert = getLicenseExpiration( - getUiSettingsService, - monitoringCluster, - getLogger, - ccrEnabled - ); - - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockReturnValue( - new Promise(resolve => { - const savedObject: SavedObject = { - id: '', - type: '', - references: [], - attributes: { - [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, - }, - }; - resolve(savedObject); - }) - ); - const services = { - callCluster: jest.fn( - (method: string, { filterPath }): Promise => { - return new Promise(resolve => { - if (filterPath.includes('hits.hits._source.license.*')) { - resolve( - fillLicense( - { - status: 'active', - type: 'gold', - expiry_date_in_millis: moment() - .add(120, 'days') - .valueOf(), - }, - clusterUuid - ) - ); - } - resolve({}); - }); - } - ), - alertInstanceFactory, - savedObjectsClient, - }; - - const state: AlertState = { - [clusterUuid]: { - expiredCheckDateMS: moment() - .subtract(1, 'day') - .valueOf(), - ui: { isFiring: true, severity: 0, message: null, resolvedMS: 0, expirationTime: 0 }, - }, + const expiryDateMS = moment() + .add(365, 'days') + .valueOf(); + const license = { + status: 'active', + type: 'gold', + expiryDateMS, + clusterUuid, }; - - const result: AlertState = (await alert.executor({ - ...alertExecutorOptions, - services, - params, - state, - })) as AlertState; - - const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + const result = await setupAlert(license, 100); + const newState = result[clusterUuid] as AlertLicensePerClusterState; expect(newState.expiredCheckDateMS).toBe(0); - expect(scheduleActions.mock.calls.length).toBe(1); - expect(scheduleActions.mock.calls[0][1].subject).toBe( - 'RESOLVED X-Pack Monitoring: License Expiration' + expect(executeActions).toHaveBeenCalledWith( + undefined, + cluster, + moment.utc(expiryDateMS), + dateFormat, + emailAddress, + true ); - expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); }); it('should not fire actions for trial license that expire in more than 14 days', async () => { - const scheduleActions = jest.fn(); - const alertInstanceFactory = jest.fn( - (id: string): AlertInstance => { - const instance = new AlertInstance(); - instance.scheduleActions = scheduleActions; - return instance; - } - ); - const alert = getLicenseExpiration( - getUiSettingsService, - monitoringCluster, - getLogger, - ccrEnabled - ); - - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockReturnValue( - new Promise(resolve => { - const savedObject: SavedObject = { - id: '', - type: '', - references: [], - attributes: { - [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, - }, - }; - resolve(savedObject); - }) - ); - const services = { - callCluster: jest.fn( - (method: string, { filterPath }): Promise => { - return new Promise(resolve => { - if (filterPath.includes('hits.hits._source.license.*')) { - resolve( - fillLicense( - { - status: 'active', - type: 'trial', - expiry_date_in_millis: moment() - .add(15, 'days') - .valueOf(), - }, - clusterUuid - ) - ); - } - resolve({}); - }); - } - ), - alertInstanceFactory, - savedObjectsClient, + const expiryDateMS = moment() + .add(20, 'days') + .valueOf(); + const license = { + status: 'active', + type: 'trial', + expiryDateMS, + clusterUuid, }; - - const state = {}; - const result: AlertState = (await alert.executor({ - ...alertExecutorOptions, - services, - params, - state, - })) as AlertState; - - const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; - expect(newState.expiredCheckDateMS).toBe(undefined); - expect(scheduleActions).not.toHaveBeenCalled(); + const result = await setupAlert(license, 0); + const newState = result[clusterUuid] as AlertLicensePerClusterState; + expect(newState.expiredCheckDateMS).toBe(0); + expect(executeActions).not.toHaveBeenCalled(); }); it('should fire actions for trial license that in 14 days or less', async () => { - const scheduleActions = jest.fn(); - const alertInstanceFactory = jest.fn( - (id: string): AlertInstance => { - const instance = new AlertInstance(); - instance.scheduleActions = scheduleActions; - return instance; - } - ); - const alert = getLicenseExpiration( - getUiSettingsService, - monitoringCluster, - getLogger, - ccrEnabled - ); - - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockReturnValue( - new Promise(resolve => { - const savedObject: SavedObject = { - id: '', - type: '', - references: [], - attributes: { - [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, - }, - }; - resolve(savedObject); - }) - ); - const services = { - callCluster: jest.fn( - (method: string, { filterPath }): Promise => { - return new Promise(resolve => { - if (filterPath.includes('hits.hits._source.license.*')) { - resolve( - fillLicense( - { - status: 'active', - type: 'trial', - expiry_date_in_millis: moment() - .add(13, 'days') - .valueOf(), - }, - clusterUuid - ) - ); - } - resolve({}); - }); - } - ), - alertInstanceFactory, - savedObjectsClient, + const expiryDateMS = moment() + .add(7, 'days') + .valueOf(); + const license = { + status: 'active', + type: 'trial', + expiryDateMS, + clusterUuid, }; - - const state = {}; - const result: AlertState = (await alert.executor({ - ...alertExecutorOptions, - services, - params, - state, - })) as AlertState; - - const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + const result = await setupAlert(license, 0); + const newState = result[clusterUuid] as AlertLicensePerClusterState; expect(newState.expiredCheckDateMS > 0).toBe(true); - expect(scheduleActions.mock.calls.length).toBe(1); + expect(executeActions).toHaveBeenCalledWith( + undefined, + cluster, + moment.utc(expiryDateMS), + dateFormat, + emailAddress + ); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts index 93397ff3641ae1..2e5356150086b4 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts @@ -5,24 +5,20 @@ */ import moment from 'moment-timezone'; -import { get } from 'lodash'; import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; import { i18n } from '@kbn/i18n'; -import { ALERT_TYPE_LICENSE_EXPIRATION, INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; import { AlertType } from '../../../../plugins/alerting/server'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; -import { fetchDefaultEmailAddress } from '../lib/alerts/fetch_default_email_address'; -import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; import { - AlertLicense, - AlertState, - AlertClusterState, - AlertClusterUiState, - LicenseExpirationAlertExecutorOptions, + AlertCommonState, + AlertLicensePerClusterState, + AlertCommonExecutorOptions, + AlertCommonCluster, + AlertLicensePerClusterUiState, } from './types'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { executeActions, getUiMessage } from '../lib/alerts/license_expiration.lib'; +import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; const EXPIRES_DAYS = [60, 30, 14, 7]; @@ -32,14 +28,6 @@ export const getLicenseExpiration = ( getLogger: (...scopes: string[]) => Logger, ccsEnabled: boolean ): AlertType => { - async function getCallCluster(services: any): Promise { - if (!monitoringCluster) { - return services.callCluster; - } - - return monitoringCluster.callAsInternalUser; - } - const logger = getLogger(ALERT_TYPE_LICENSE_EXPIRATION); return { id: ALERT_TYPE_LICENSE_EXPIRATION, @@ -53,54 +41,50 @@ export const getLicenseExpiration = ( }, ], defaultActionGroupId: 'default', - async executor({ - services, - params, - state, - }: LicenseExpirationAlertExecutorOptions): Promise { + async executor({ services, params, state }: AlertCommonExecutorOptions): Promise { logger.debug( `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` ); - const callCluster = await getCallCluster(services); - - // Support CCS use cases by querying to find available remote clusters - // and then adding those to the index pattern we are searching against - let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; - if (ccsEnabled) { - const availableCcs = await fetchAvailableCcs(callCluster); - if (availableCcs.length > 0) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } - } - - const clusters = await fetchClusters(callCluster, esIndexPattern); + const preparedAlert = await getPreparedAlert( + ALERT_TYPE_LICENSE_EXPIRATION, + getUiSettingsService, + monitoringCluster, + logger, + ccsEnabled, + services, + fetchLicenses + ); - // Fetch licensing information from cluster_stats documents - const licenses: AlertLicense[] = await fetchLicenses(callCluster, clusters, esIndexPattern); - if (licenses.length === 0) { - logger.warn(`No license found for ${ALERT_TYPE_LICENSE_EXPIRATION}.`); + if (!preparedAlert) { return state; } - const uiSettings = (await getUiSettingsService()).asScopedToClient( - services.savedObjectsClient - ); - const dateFormat: string = await uiSettings.get('dateFormat'); - const timezone: string = await uiSettings.get('dateFormat:tz'); - const emailAddress = await fetchDefaultEmailAddress(uiSettings); - if (!emailAddress) { - // TODO: we can do more here - logger.warn( - `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` - ); - return; - } + const { emailAddress, data: licenses, clusters, dateFormat } = preparedAlert; - const result: AlertState = { ...state }; + const result: AlertCommonState = { ...state }; + const defaultAlertState: AlertLicensePerClusterState = { + expiredCheckDateMS: 0, + ui: { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + lastCheckedMS: 0, + triggeredMS: 0, + }, + }; for (const license of licenses) { - const licenseState: AlertClusterState = state[license.clusterUuid] || {}; + const alertState: AlertLicensePerClusterState = + (state[license.clusterUuid] as AlertLicensePerClusterState) || defaultAlertState; + const cluster = clusters.find( + (c: AlertCommonCluster) => c.clusterUuid === license.clusterUuid + ); + if (!cluster) { + logger.warn(`Unable to find cluster for clusterUuid='${license.clusterUuid}'`); + continue; + } const $expiry = moment.utc(license.expiryDateMS); let isExpired = false; let severity = 0; @@ -123,31 +107,26 @@ export const getLicenseExpiration = ( } } - const ui: AlertClusterUiState = get(licenseState, 'ui', { - isFiring: false, - message: null, - severity: 0, - resolvedMS: 0, - expirationTime: 0, - }); + const ui = alertState.ui; + let triggered = ui.triggeredMS; let resolved = ui.resolvedMS; let message = ui.message; - let expiredCheckDate = licenseState.expiredCheckDateMS; + let expiredCheckDate = alertState.expiredCheckDateMS; const instance = services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION); if (isExpired) { - if (!licenseState.expiredCheckDateMS) { + if (!alertState.expiredCheckDateMS) { logger.debug(`License will expire soon, sending email`); - executeActions(instance, license, $expiry, dateFormat, emailAddress); - expiredCheckDate = moment().valueOf(); + executeActions(instance, cluster, $expiry, dateFormat, emailAddress); + expiredCheckDate = triggered = moment().valueOf(); } - message = getUiMessage(license, timezone); + message = getUiMessage(); resolved = 0; - } else if (!isExpired && licenseState.expiredCheckDateMS) { + } else if (!isExpired && alertState.expiredCheckDateMS) { logger.debug(`License expiration has been resolved, sending email`); - executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + executeActions(instance, cluster, $expiry, dateFormat, emailAddress, true); expiredCheckDate = 0; - message = getUiMessage(license, timezone, true); + message = getUiMessage(true); resolved = moment().valueOf(); } @@ -159,8 +138,10 @@ export const getLicenseExpiration = ( isFiring: expiredCheckDate > 0, severity, resolvedMS: resolved, - }, - }; + triggeredMS: triggered, + lastCheckedMS: moment().valueOf(), + } as AlertLicensePerClusterUiState, + } as AlertLicensePerClusterState; } return result; diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts index ff47d6f2ad4dca..b689d008b51a78 100644 --- a/x-pack/plugins/monitoring/server/alerts/types.d.ts +++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts @@ -5,41 +5,79 @@ */ import { Moment } from 'moment'; import { AlertExecutorOptions } from '../../../alerting/server'; +import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from './enums'; export interface AlertLicense { status: string; type: string; expiryDateMS: number; clusterUuid: string; - clusterName: string; } -export interface AlertState { - [clusterUuid: string]: AlertClusterState; +export interface AlertClusterState { + state: AlertClusterStateState; + clusterUuid: string; +} + +export interface AlertCommonState { + [clusterUuid: string]: AlertCommonPerClusterState; } -export interface AlertClusterState { - expiredCheckDateMS: number | Moment; - ui: AlertClusterUiState; +export interface AlertCommonPerClusterState { + ui: AlertCommonPerClusterUiState; } -export interface AlertClusterUiState { +export interface AlertClusterStatePerClusterState extends AlertCommonPerClusterState { + state: AlertClusterStateState; +} + +export interface AlertLicensePerClusterState extends AlertCommonPerClusterState { + expiredCheckDateMS: number; +} + +export interface AlertCommonPerClusterUiState { isFiring: boolean; severity: number; - message: string | null; + message: AlertCommonPerClusterMessage | null; resolvedMS: number; + lastCheckedMS: number; + triggeredMS: number; +} + +export interface AlertCommonPerClusterMessage { + text: string; // Do this. #link this is a link #link + tokens?: AlertCommonPerClusterMessageToken[]; +} + +export interface AlertCommonPerClusterMessageToken { + startToken: string; + endToken?: string; + type: AlertCommonPerClusterMessageTokenType; +} + +export interface AlertCommonPerClusterMessageLinkToken extends AlertCommonPerClusterMessageToken { + url?: string; +} + +export interface AlertCommonPerClusterMessageTimeToken extends AlertCommonPerClusterMessageToken { + isRelative: boolean; + isAbsolute: boolean; +} + +export interface AlertLicensePerClusterUiState extends AlertCommonPerClusterUiState { expirationTime: number; } -export interface AlertCluster { +export interface AlertCommonCluster { clusterUuid: string; + clusterName: string; } -export interface LicenseExpirationAlertExecutorOptions extends AlertExecutorOptions { - state: AlertState; +export interface AlertCommonExecutorOptions extends AlertExecutorOptions { + state: AlertCommonState; } -export interface AlertParams { +export interface AlertCommonParams { dateFormat: string; timezone: string; } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts deleted file mode 100644 index 2c40ac56e19ec5..00000000000000 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts +++ /dev/null @@ -1,86 +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 { get, snakeCase } from 'lodash'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { KIBANA_USAGE_TYPE, KIBANA_STATS_TYPE_MONITORING } from '../../../common/constants'; - -const TYPES = [ - 'dashboard', - 'visualization', - 'search', - 'index-pattern', - 'graph-workspace', - 'timelion-sheet', -]; - -/** - * Fetches saved object counts by querying the .kibana index - */ -export function getKibanaUsageCollector(usageCollection: any, kibanaIndex: string) { - return usageCollection.makeUsageCollector({ - type: KIBANA_USAGE_TYPE, - isReady: () => true, - async fetch(callCluster: CallCluster) { - const savedObjectCountSearchParams = { - index: kibanaIndex, - ignoreUnavailable: true, - filterPath: 'aggregations.types.buckets', - body: { - size: 0, - query: { - terms: { type: TYPES }, - }, - aggs: { - types: { - terms: { field: 'type', size: TYPES.length }, - }, - }, - }, - }; - - const resp = await callCluster('search', savedObjectCountSearchParams); - const buckets: any = get(resp, 'aggregations.types.buckets', []); - - // get the doc_count from each bucket - const bucketCounts = buckets.reduce( - (acc: any, bucket: any) => ({ - ...acc, - [bucket.key]: bucket.doc_count, - }), - {} - ); - - return { - index: kibanaIndex, - ...TYPES.reduce( - (acc, type) => ({ - // combine the bucketCounts and 0s for types that don't have documents - ...acc, - [snakeCase(type)]: { - total: bucketCounts[type] || 0, - }, - }), - {} - ), - }; - }, - - /* - * Format the response data into a model for internal upload - * 1. Make this data part of the "kibana_stats" type - * 2. Organize the payload in the usage namespace of the data payload (usage.index, etc) - */ - formatForBulkUpload: (result: any) => { - return { - type: KIBANA_STATS_TYPE_MONITORING, - payload: { - usage: result, - }, - }; - }, - }); -} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.ts deleted file mode 100644 index 85357f786ddc17..00000000000000 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.ts +++ /dev/null @@ -1,46 +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 { Observable } from 'rxjs'; -import { cloneDeep } from 'lodash'; -import moment from 'moment'; -import { OpsMetrics } from 'kibana/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { KIBANA_STATS_TYPE_MONITORING } from '../../../common/constants'; - -interface MonitoringOpsMetrics extends OpsMetrics { - timestamp: string; -} - -/* - * Initialize a collector for Kibana Ops Stats - */ -export function getOpsStatsCollector( - usageCollection: UsageCollectionSetup, - metrics$: Observable -) { - let lastMetrics: MonitoringOpsMetrics | null = null; - metrics$.subscribe(_metrics => { - const metrics: any = cloneDeep(_metrics); - // Ensure we only include the same data that Metricbeat collection would get - delete metrics.process.pid; - metrics.response_times = { - average: metrics.response_times.avg_in_millis, - max: metrics.response_times.max_in_millis, - }; - delete metrics.requests.statusCodes; - lastMetrics = { - ...metrics, - timestamp: moment.utc().toISOString(), - }; - }); - - return usageCollection.makeStatsCollector({ - type: KIBANA_STATS_TYPE_MONITORING, - isReady: () => !!lastMetrics, - fetch: () => lastMetrics, - }); -} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts index e41b1512f1b299..dcd35b0d323ebf 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts @@ -3,20 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Observable } from 'rxjs'; -import { OpsMetrics } from 'kibana/server'; -import { getKibanaUsageCollector } from './get_kibana_usage_collector'; -import { getOpsStatsCollector } from './get_ops_stats_collector'; + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { getSettingsCollector } from './get_settings_collector'; import { MonitoringConfig } from '../../config'; export function registerCollectors( - usageCollection: any, - config: MonitoringConfig, - opsMetrics$: Observable, - kibanaIndex: string + usageCollection: UsageCollectionSetup, + config: MonitoringConfig ) { - usageCollection.registerCollector(getOpsStatsCollector(usageCollection, opsMetrics$)); - usageCollection.registerCollector(getKibanaUsageCollector(usageCollection, kibanaIndex)); usageCollection.registerCollector(getSettingsCollector(usageCollection, config)); } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts new file mode 100644 index 00000000000000..81e375734cc507 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { executeActions, getUiMessage } from './cluster_state.lib'; +import { AlertClusterStateState } from '../../alerts/enums'; +import { AlertCommonPerClusterMessageLinkToken } from '../../alerts/types'; + +describe('clusterState lib', () => { + describe('executeActions', () => { + const clusterName = 'clusterA'; + const instance: any = { scheduleActions: jest.fn() }; + const license: any = { clusterName }; + const status = AlertClusterStateState.Green; + const emailAddress = 'test@test.com'; + + beforeEach(() => { + instance.scheduleActions.mockClear(); + }); + + it('should schedule actions when firing', () => { + executeActions(instance, license, status, emailAddress, false); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'NEW X-Pack Monitoring: Cluster Status', + message: `Allocate missing replica shards for cluster '${clusterName}'`, + to: emailAddress, + }); + }); + + it('should have a different message for red state', () => { + executeActions(instance, license, AlertClusterStateState.Red, emailAddress, false); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'NEW X-Pack Monitoring: Cluster Status', + message: `Allocate missing primary and replica shards for cluster '${clusterName}'`, + to: emailAddress, + }); + }); + + it('should schedule actions when resolved', () => { + executeActions(instance, license, status, emailAddress, true); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'RESOLVED X-Pack Monitoring: Cluster Status', + message: `This cluster alert has been resolved: Allocate missing replica shards for cluster '${clusterName}'`, + to: emailAddress, + }); + }); + }); + + describe('getUiMessage', () => { + it('should return a message when firing', () => { + const message = getUiMessage(AlertClusterStateState.Red, false); + expect(message.text).toBe( + `Elasticsearch cluster status is red. #start_linkAllocate missing primary and replica shards#end_link` + ); + expect(message.tokens && message.tokens.length).toBe(1); + expect(message.tokens && message.tokens[0].startToken).toBe('#start_link'); + expect(message.tokens && message.tokens[0].endToken).toBe('#end_link'); + expect( + message.tokens && (message.tokens[0] as AlertCommonPerClusterMessageLinkToken).url + ).toBe('elasticsearch/indices'); + }); + + it('should return a message when resolved', () => { + const message = getUiMessage(AlertClusterStateState.Green, true); + expect(message.text).toBe(`Elasticsearch cluster status is green.`); + expect(message.tokens).not.toBeDefined(); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts new file mode 100644 index 00000000000000..ae66d603507ca6 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts @@ -0,0 +1,88 @@ +/* + * 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 { AlertInstance } from '../../../../alerting/server'; +import { + AlertCommonCluster, + AlertCommonPerClusterMessage, + AlertCommonPerClusterMessageLinkToken, +} from '../../alerts/types'; +import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from '../../alerts/enums'; + +const RESOLVED_SUBJECT = i18n.translate('xpack.monitoring.alerts.clusterStatus.resolvedSubject', { + defaultMessage: 'RESOLVED X-Pack Monitoring: Cluster Status', +}); + +const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.clusterStatus.newSubject', { + defaultMessage: 'NEW X-Pack Monitoring: Cluster Status', +}); + +const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterStatus.redMessage', { + defaultMessage: 'Allocate missing primary and replica shards', +}); + +const YELLOW_STATUS_MESSAGE = i18n.translate( + 'xpack.monitoring.alerts.clusterStatus.yellowMessage', + { + defaultMessage: 'Allocate missing replica shards', + } +); + +export function executeActions( + instance: AlertInstance, + cluster: AlertCommonCluster, + status: AlertClusterStateState, + emailAddress: string, + resolved: boolean = false +) { + const message = + status === AlertClusterStateState.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE; + if (resolved) { + instance.scheduleActions('default', { + subject: RESOLVED_SUBJECT, + message: `This cluster alert has been resolved: ${message} for cluster '${cluster.clusterName}'`, + to: emailAddress, + }); + } else { + instance.scheduleActions('default', { + subject: NEW_SUBJECT, + message: `${message} for cluster '${cluster.clusterName}'`, + to: emailAddress, + }); + } +} + +export function getUiMessage( + status: AlertClusterStateState, + resolved: boolean = false +): AlertCommonPerClusterMessage { + if (resolved) { + return { + text: i18n.translate('xpack.monitoring.alerts.clusterStatus.ui.resolvedMessage', { + defaultMessage: `Elasticsearch cluster status is green.`, + }), + }; + } + const message = + status === AlertClusterStateState.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE; + return { + text: i18n.translate('xpack.monitoring.alerts.clusterStatus.ui.firingMessage', { + defaultMessage: `Elasticsearch cluster status is {status}. #start_link{message}#end_link`, + values: { + status, + message, + }, + }), + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertCommonPerClusterMessageTokenType.Link, + url: 'elasticsearch/indices', + } as AlertCommonPerClusterMessageLinkToken, + ], + }; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts new file mode 100644 index 00000000000000..642ae3c39a0275 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fetchClusterState } from './fetch_cluster_state'; + +describe('fetchClusterState', () => { + it('should return the cluster state', async () => { + const status = 'green'; + const clusterUuid = 'sdfdsaj34434'; + const callCluster = jest.fn(() => ({ + hits: { + hits: [ + { + _source: { + cluster_state: { + status, + }, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + + const clusters = [{ clusterUuid, clusterName: 'foo' }]; + const index = '.monitoring-es-*'; + + const state = await fetchClusterState(callCluster, clusters, index); + expect(state).toEqual([ + { + state: status, + clusterUuid, + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts new file mode 100644 index 00000000000000..66ea30d5f2e96a --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { AlertCommonCluster, AlertClusterState } from '../../alerts/types'; + +export async function fetchClusterState( + callCluster: any, + clusters: AlertCommonCluster[], + index: string +): Promise { + const params = { + index, + filterPath: ['hits.hits._source.cluster_state.status', 'hits.hits._source.cluster_uuid'], + body: { + size: 1, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map(cluster => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'hits.hits', []).map((hit: any) => { + return { + state: get(hit, '_source.cluster_state.status'), + clusterUuid: get(hit, '_source.cluster_uuid'), + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts index 78eb9773df15fb..7a9b61f37707ba 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -6,21 +6,51 @@ import { fetchClusters } from './fetch_clusters'; describe('fetchClusters', () => { + const clusterUuid = '1sdfds734'; + const clusterName = 'monitoring'; + it('return a list of clusters', async () => { const callCluster = jest.fn().mockImplementation(() => ({ - aggregations: { - clusters: { - buckets: [ - { - key: 'clusterA', + hits: { + hits: [ + { + _source: { + cluster_uuid: clusterUuid, + cluster_name: clusterName, + }, + }, + ], + }, + })); + const index = '.monitoring-es-*'; + const result = await fetchClusters(callCluster, index); + expect(result).toEqual([{ clusterUuid, clusterName }]); + }); + + it('return the metadata name if available', async () => { + const metadataName = 'custom-monitoring'; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + cluster_uuid: clusterUuid, + cluster_name: clusterName, + cluster_settings: { + cluster: { + metadata: { + display_name: metadataName, + }, + }, + }, }, - ], - }, + }, + ], }, })); const index = '.monitoring-es-*'; const result = await fetchClusters(callCluster, index); - expect(result).toEqual([{ clusterUuid: 'clusterA' }]); + expect(result).toEqual([{ clusterUuid, clusterName: metadataName }]); }); it('should limit the time period in the query', async () => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index 8ef7339618a2c0..d1513ac16fb158 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -4,18 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { AlertCluster } from '../../alerts/types'; +import { AlertCommonCluster } from '../../alerts/types'; -interface AggregationResult { - key: string; -} - -export async function fetchClusters(callCluster: any, index: string): Promise { +export async function fetchClusters( + callCluster: any, + index: string +): Promise { const params = { index, - filterPath: 'aggregations.clusters.buckets', + filterPath: [ + 'hits.hits._source.cluster_settings.cluster.metadata.display_name', + 'hits.hits._source.cluster_uuid', + 'hits.hits._source.cluster_name', + ], body: { - size: 0, + size: 1000, query: { bool: { filter: [ @@ -34,19 +37,21 @@ export async function fetchClusters(callCluster: any, index: string): Promise ({ - clusterUuid: bucket.key, - })); + return get(response, 'hits.hits', []).map((hit: any) => { + const clusterName: string = + get(hit, '_source.cluster_settings.cluster.metadata.display_name') || + get(hit, '_source.cluster_name') || + get(hit, '_source.cluster_uuid'); + return { + clusterUuid: get(hit, '_source.cluster_uuid'), + clusterName, + }; + }); } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts index dd6c074e68b1f9..9dcb4ffb82a5fc 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -6,28 +6,28 @@ import { fetchLicenses } from './fetch_licenses'; describe('fetchLicenses', () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + it('return a list of licenses', async () => { - const clusterName = 'MyCluster'; - const clusterUuid = 'clusterA'; - const license = { - status: 'active', - expiry_date_in_millis: 1579532493876, - type: 'basic', - }; const callCluster = jest.fn().mockImplementation(() => ({ hits: { hits: [ { _source: { license, - cluster_name: clusterName, cluster_uuid: clusterUuid, }, }, ], }, })); - const clusters = [{ clusterUuid }]; + const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; const result = await fetchLicenses(callCluster, clusters, index); expect(result).toEqual([ @@ -36,15 +36,13 @@ describe('fetchLicenses', () => { type: license.type, expiryDateMS: license.expiry_date_in_millis, clusterUuid, - clusterName, }, ]); }); it('should only search for the clusters provided', async () => { - const clusterUuid = 'clusterA'; const callCluster = jest.fn(); - const clusters = [{ clusterUuid }]; + const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; await fetchLicenses(callCluster, clusters, index); const params = callCluster.mock.calls[0][1]; @@ -52,54 +50,11 @@ describe('fetchLicenses', () => { }); it('should limit the time period in the query', async () => { - const clusterUuid = 'clusterA'; const callCluster = jest.fn(); - const clusters = [{ clusterUuid }]; + const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; await fetchLicenses(callCluster, clusters, index); const params = callCluster.mock.calls[0][1]; expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); }); - - it('should give priority to the metadata name', async () => { - const clusterName = 'MyCluster'; - const clusterUuid = 'clusterA'; - const license = { - status: 'active', - expiry_date_in_millis: 1579532493876, - type: 'basic', - }; - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - license, - cluster_name: 'fakeName', - cluster_uuid: clusterUuid, - cluster_settings: { - cluster: { - metadata: { - display_name: clusterName, - }, - }, - }, - }, - }, - ], - }, - })); - const clusters = [{ clusterUuid }]; - const index = '.monitoring-es-*'; - const result = await fetchLicenses(callCluster, clusters, index); - expect(result).toEqual([ - { - status: license.status, - type: license.type, - expiryDateMS: license.expiry_date_in_millis, - clusterUuid, - clusterName, - }, - ]); - }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 31a68e8aa9c3ee..5b05c907e796e8 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -4,21 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { AlertLicense, AlertCluster } from '../../alerts/types'; +import { AlertLicense, AlertCommonCluster } from '../../alerts/types'; export async function fetchLicenses( callCluster: any, - clusters: AlertCluster[], + clusters: AlertCommonCluster[], index: string ): Promise { const params = { index, - filterPath: [ - 'hits.hits._source.license.*', - 'hits.hits._source.cluster_settings.cluster.metadata.display_name', - 'hits.hits._source.cluster_uuid', - 'hits.hits._source.cluster_name', - ], + filterPath: ['hits.hits._source.license.*', 'hits.hits._source.cluster_uuid'], body: { size: 1, sort: [{ timestamp: { order: 'desc' } }], @@ -50,17 +45,12 @@ export async function fetchLicenses( const response = await callCluster('search', params); return get(response, 'hits.hits', []).map((hit: any) => { - const clusterName: string = - get(hit, '_source.cluster_settings.cluster.metadata.display_name') || - get(hit, '_source.cluster_name') || - get(hit, '_source.cluster_uuid'); const rawLicense: any = get(hit, '_source.license', {}); const license: AlertLicense = { status: rawLicense.status, type: rawLicense.type, expiryDateMS: rawLicense.expiry_date_in_millis, clusterUuid: get(hit, '_source.cluster_uuid'), - clusterName, }; return license; }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts new file mode 100644 index 00000000000000..a3bcb61afacd61 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -0,0 +1,122 @@ +/* + * 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 { fetchStatus } from './fetch_status'; +import { AlertCommonPerClusterState } from '../../alerts/types'; + +describe('fetchStatus', () => { + const alertType = 'monitoringTest'; + const log = { warn: jest.fn() }; + const start = 0; + const end = 0; + const id = 1; + const defaultUiState = { + isFiring: false, + severity: 0, + message: null, + resolvedMS: 0, + lastCheckedMS: 0, + triggeredMS: 0, + }; + const alertsClient = { + find: jest.fn(() => ({ + total: 1, + data: [ + { + id, + }, + ], + })), + getAlertState: jest.fn(() => ({ + alertTypeState: { + state: { + ui: defaultUiState, + } as AlertCommonPerClusterState, + }, + })), + }; + + afterEach(() => { + (alertsClient.find as jest.Mock).mockClear(); + (alertsClient.getAlertState as jest.Mock).mockClear(); + }); + + it('should fetch from the alerts client', async () => { + const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); + expect(status).toEqual([]); + }); + + it('should return alerts that are firing', async () => { + alertsClient.getAlertState = jest.fn(() => ({ + alertTypeState: { + state: { + ui: { + ...defaultUiState, + isFiring: true, + }, + } as AlertCommonPerClusterState, + }, + })); + + const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); + expect(status.length).toBe(1); + expect(status[0].type).toBe(alertType); + expect(status[0].isFiring).toBe(true); + }); + + it('should return alerts that have been resolved in the time period', async () => { + alertsClient.getAlertState = jest.fn(() => ({ + alertTypeState: { + state: { + ui: { + ...defaultUiState, + resolvedMS: 1500, + }, + } as AlertCommonPerClusterState, + }, + })); + + const customStart = 1000; + const customEnd = 2000; + + const status = await fetchStatus( + alertsClient as any, + [alertType], + customStart, + customEnd, + log as any + ); + expect(status.length).toBe(1); + expect(status[0].type).toBe(alertType); + expect(status[0].isFiring).toBe(false); + }); + + it('should pass in the right filter to the alerts client', async () => { + await fetchStatus(alertsClient as any, [alertType], start, end, log as any); + expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( + `alert.attributes.alertTypeId:${alertType}` + ); + }); + + it('should return nothing if no alert state is found', async () => { + alertsClient.getAlertState = jest.fn(() => ({ + alertTypeState: null, + })) as any; + + const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); + expect(status).toEqual([]); + }); + + it('should return nothing if no alerts are found', async () => { + alertsClient.find = jest.fn(() => ({ + total: 0, + data: [], + })) as any; + + const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); + expect(status).toEqual([]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 9f7c1d5a994d27..bf6ee965d3b2fb 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -4,81 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ import moment from 'moment'; -import { get } from 'lodash'; -import { AlertClusterState } from '../../alerts/types'; -import { ALERT_TYPES, LOGGING_TAG } from '../../../common/constants'; +import { Logger } from '../../../../../../src/core/server'; +import { AlertCommonPerClusterState } from '../../alerts/types'; +import { AlertsClient } from '../../../../alerting/server'; export async function fetchStatus( - callCluster: any, + alertsClient: AlertsClient, + alertTypes: string[], start: number, end: number, - clusterUuid: string, - server: any + log: Logger ): Promise { - // TODO: this shouldn't query task manager directly but rather - // use an api exposed by the alerting/actions plugin - // See https://github.com/elastic/kibana/issues/48442 const statuses = await Promise.all( - ALERT_TYPES.map( + alertTypes.map( type => new Promise(async (resolve, reject) => { - try { - const params = { - index: '.kibana_task_manager', - filterPath: ['hits.hits._source.task.state'], - body: { - size: 1, - sort: [{ updated_at: { order: 'desc' } }], - query: { - bool: { - filter: [ - { - term: { - 'task.taskType': `alerting:${type}`, - }, - }, - ], - }, - }, - }, - }; - - const response = await callCluster('search', params); - const state = get(response, 'hits.hits[0]._source.task.state', '{}'); - const clusterState: AlertClusterState = get( - JSON.parse(state), - `alertTypeState.${clusterUuid}`, - { - expiredCheckDateMS: 0, - ui: { - isFiring: false, - message: null, - severity: 0, - resolvedMS: 0, - expirationTime: 0, - }, - } - ); - const isInBetween = moment(clusterState.ui.resolvedMS).isBetween(start, end); - if (clusterState.ui.isFiring || isInBetween) { - return resolve({ - type, - ...clusterState.ui, - }); - } + // We need to get the id from the alertTypeId + const alerts = await alertsClient.find({ + options: { + filter: `alert.attributes.alertTypeId:${type}`, + }, + }); + if (alerts.total === 0) { return resolve(false); - } catch (err) { - const reason = get(err, 'body.error.type'); - if (reason === 'index_not_found_exception') { - server.log( - ['error', LOGGING_TAG], - `Unable to fetch alerts. Alerts depends on task manager, which has not been started yet.` - ); - } else { - server.log(['error', LOGGING_TAG], err.message); - } + } + + if (alerts.total !== 1) { + log.warn(`Found more than one alert for type ${type} which is unexpected.`); + } + + const id = alerts.data[0].id; + + // Now that we have the id, we can get the state + const states = await alertsClient.getAlertState({ id }); + if (!states || !states.alertTypeState) { + log.warn(`No alert states found for type ${type} which is unexpected.`); return resolve(false); } + + const state = Object.values(states.alertTypeState)[0] as AlertCommonPerClusterState; + const isInBetween = moment(state.ui.resolvedMS).isBetween(start, end); + if (state.ui.isFiring || isInBetween) { + return resolve({ + type, + ...state.ui, + }); + } + return resolve(false); }) ) ); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts new file mode 100644 index 00000000000000..1840a2026a7534 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts @@ -0,0 +1,163 @@ +/* + * 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 { getPreparedAlert } from './get_prepared_alert'; +import { fetchClusters } from './fetch_clusters'; +import { fetchDefaultEmailAddress } from './fetch_default_email_address'; + +jest.mock('./fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +jest.mock('./fetch_default_email_address', () => ({ + fetchDefaultEmailAddress: jest.fn(), +})); + +describe('getPreparedAlert', () => { + const uiSettings = { get: jest.fn() }; + const alertType = 'test'; + const getUiSettingsService = async () => ({ + asScopedToClient: () => uiSettings, + }); + const monitoringCluster = null; + const logger = { warn: jest.fn() }; + const ccsEnabled = false; + const services = { + callCluster: jest.fn(), + savedObjectsClient: null, + }; + const emailAddress = 'foo@foo.com'; + const data = [{ foo: 1 }]; + const dataFetcher = () => data; + const clusterName = 'MonitoringCluster'; + const clusterUuid = 'sdf34sdf'; + const clusters = [{ clusterName, clusterUuid }]; + + afterEach(() => { + (uiSettings.get as jest.Mock).mockClear(); + (services.callCluster as jest.Mock).mockClear(); + (fetchClusters as jest.Mock).mockClear(); + (fetchDefaultEmailAddress as jest.Mock).mockClear(); + }); + + beforeEach(() => { + (fetchClusters as jest.Mock).mockImplementation(() => clusters); + (fetchDefaultEmailAddress as jest.Mock).mockImplementation(() => emailAddress); + }); + + it('should return fields as expected', async () => { + (uiSettings.get as jest.Mock).mockImplementation(() => { + return emailAddress; + }); + + const alert = await getPreparedAlert( + alertType, + getUiSettingsService as any, + monitoringCluster as any, + logger as any, + ccsEnabled, + services as any, + dataFetcher as any + ); + + expect(alert && alert.emailAddress).toBe(emailAddress); + expect(alert && alert.data).toBe(data); + }); + + it('should add ccs if specified', async () => { + const ccsClusterName = 'remoteCluster'; + (services.callCluster as jest.Mock).mockImplementation(() => { + return { + [ccsClusterName]: { + connected: true, + }, + }; + }); + + await getPreparedAlert( + alertType, + getUiSettingsService as any, + monitoringCluster as any, + logger as any, + true, + services as any, + dataFetcher as any + ); + + expect((fetchClusters as jest.Mock).mock.calls[0][1].includes(ccsClusterName)).toBe(true); + }); + + it('should ignore ccs if no remote clusters are available', async () => { + const ccsClusterName = 'remoteCluster'; + (services.callCluster as jest.Mock).mockImplementation(() => { + return { + [ccsClusterName]: { + connected: false, + }, + }; + }); + + await getPreparedAlert( + alertType, + getUiSettingsService as any, + monitoringCluster as any, + logger as any, + true, + services as any, + dataFetcher as any + ); + + expect((fetchClusters as jest.Mock).mock.calls[0][1].includes(ccsClusterName)).toBe(false); + }); + + it('should pass in the clusters into the data fetcher', async () => { + const customDataFetcher = jest.fn(() => data); + + await getPreparedAlert( + alertType, + getUiSettingsService as any, + monitoringCluster as any, + logger as any, + true, + services as any, + customDataFetcher as any + ); + + expect((customDataFetcher as jest.Mock).mock.calls[0][1]).toBe(clusters); + }); + + it('should return nothing if the data fetcher returns nothing', async () => { + const customDataFetcher = jest.fn(() => []); + + const result = await getPreparedAlert( + alertType, + getUiSettingsService as any, + monitoringCluster as any, + logger as any, + true, + services as any, + customDataFetcher as any + ); + + expect(result).toBe(null); + }); + + it('should return nothing if there is no email address', async () => { + (fetchDefaultEmailAddress as jest.Mock).mockImplementation(() => null); + + const result = await getPreparedAlert( + alertType, + getUiSettingsService as any, + monitoringCluster as any, + logger as any, + true, + services as any, + dataFetcher as any + ); + + expect(result).toBe(null); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts new file mode 100644 index 00000000000000..83a9e26e4c5890 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts @@ -0,0 +1,87 @@ +/* + * 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 { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'kibana/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { AlertServices } from '../../../../alerting/server'; +import { AlertCommonCluster } from '../../alerts/types'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants'; +import { fetchAvailableCcs } from './fetch_available_ccs'; +import { getCcsIndexPattern } from './get_ccs_index_pattern'; +import { fetchClusters } from './fetch_clusters'; +import { fetchDefaultEmailAddress } from './fetch_default_email_address'; + +export interface PreparedAlert { + emailAddress: string; + clusters: AlertCommonCluster[]; + data: any[]; + timezone: string; + dateFormat: string; +} + +async function getCallCluster( + monitoringCluster: ICustomClusterClient, + services: Pick +): Promise { + if (!monitoringCluster) { + return services.callCluster; + } + + return monitoringCluster.callAsInternalUser; +} + +export async function getPreparedAlert( + alertType: string, + getUiSettingsService: () => Promise, + monitoringCluster: ICustomClusterClient, + logger: Logger, + ccsEnabled: boolean, + services: Pick, + dataFetcher: ( + callCluster: CallCluster, + clusters: AlertCommonCluster[], + esIndexPattern: string + ) => Promise +): Promise { + const callCluster = await getCallCluster(monitoringCluster, services); + + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (ccsEnabled) { + const availableCcs = await fetchAvailableCcs(callCluster); + if (availableCcs.length > 0) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + } + + const clusters = await fetchClusters(callCluster, esIndexPattern); + + // Fetch the specific data + const data = await dataFetcher(callCluster, clusters, esIndexPattern); + if (data.length === 0) { + logger.warn(`No data found for ${alertType}.`); + return null; + } + + const uiSettings = (await getUiSettingsService()).asScopedToClient(services.savedObjectsClient); + const dateFormat: string = await uiSettings.get('dateFormat'); + const timezone: string = await uiSettings.get('dateFormat:tz'); + const emailAddress = await fetchDefaultEmailAddress(uiSettings); + if (!emailAddress) { + // TODO: we can do more here + logger.warn(`Unable to send email for ${alertType} because there is no email configured.`); + return null; + } + + return { + emailAddress, + data, + clusters, + dateFormat, + timezone, + }; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts index 1a2eb1e44be843..b99208bdde2c86 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts @@ -39,17 +39,26 @@ describe('licenseExpiration lib', () => { }); describe('getUiMessage', () => { - const timezone = 'Europe/London'; - const license: any = { expiryDateMS: moment.tz('2020-01-20 08:00:00', timezone).utc() }; - it('should return a message when firing', () => { - const message = getUiMessage(license, timezone, false); - expect(message).toBe(`This cluster's license is going to expire in #relative at #absolute.`); + const message = getUiMessage(false); + expect(message.text).toBe( + `This cluster's license is going to expire in #relative at #absolute. #start_linkPlease update your license.#end_link` + ); + // LOL How do I avoid this in TS???? + if (!message.tokens) { + return expect(false).toBe(true); + } + expect(message.tokens.length).toBe(3); + expect(message.tokens[0].startToken).toBe('#relative'); + expect(message.tokens[1].startToken).toBe('#absolute'); + expect(message.tokens[2].startToken).toBe('#start_link'); + expect(message.tokens[2].endToken).toBe('#end_link'); }); it('should return a message when resolved', () => { - const message = getUiMessage(license, timezone, true); - expect(message).toBe(`This cluster's license is active.`); + const message = getUiMessage(true); + expect(message.text).toBe(`This cluster's license is active.`); + expect(message.tokens).not.toBeDefined(); }); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts index 41b68d69bbd25f..cfe9f02b9bd6ae 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts @@ -6,7 +6,13 @@ import { Moment } from 'moment-timezone'; import { i18n } from '@kbn/i18n'; import { AlertInstance } from '../../../../alerting/server'; -import { AlertLicense } from '../../alerts/types'; +import { + AlertCommonPerClusterMessageLinkToken, + AlertCommonPerClusterMessageTimeToken, + AlertCommonCluster, + AlertCommonPerClusterMessage, +} from '../../alerts/types'; +import { AlertCommonPerClusterMessageTokenType } from '../../alerts/enums'; const RESOLVED_SUBJECT = i18n.translate( 'xpack.monitoring.alerts.licenseExpiration.resolvedSubject', @@ -21,7 +27,7 @@ const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.licenseExpiration.ne export function executeActions( instance: AlertInstance, - license: AlertLicense, + cluster: AlertCommonCluster, $expiry: Moment, dateFormat: string, emailAddress: string, @@ -31,14 +37,14 @@ export function executeActions( instance.scheduleActions('default', { subject: RESOLVED_SUBJECT, message: `This cluster alert has been resolved: Cluster '${ - license.clusterName + cluster.clusterName }' license was going to expire on ${$expiry.format(dateFormat)}.`, to: emailAddress, }); } else { instance.scheduleActions('default', { subject: NEW_SUBJECT, - message: `Cluster '${license.clusterName}' license is going to expire on ${$expiry.format( + message: `Cluster '${cluster.clusterName}' license is going to expire on ${$expiry.format( dateFormat )}. Please update your license.`, to: emailAddress, @@ -46,13 +52,37 @@ export function executeActions( } } -export function getUiMessage(license: AlertLicense, timezone: string, resolved: boolean = false) { +export function getUiMessage(resolved: boolean = false): AlertCommonPerClusterMessage { if (resolved) { - return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { - defaultMessage: `This cluster's license is active.`, - }); + return { + text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { + defaultMessage: `This cluster's license is active.`, + }), + }; } - return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { - defaultMessage: `This cluster's license is going to expire in #relative at #absolute.`, - }); + return { + text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { + defaultMessage: `This cluster's license is going to expire in #relative at #absolute. #start_linkPlease update your license.#end_link`, + }), + tokens: [ + { + startToken: '#relative', + type: AlertCommonPerClusterMessageTokenType.Time, + isRelative: true, + isAbsolute: false, + } as AlertCommonPerClusterMessageTimeToken, + { + startToken: '#absolute', + type: AlertCommonPerClusterMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + } as AlertCommonPerClusterMessageTimeToken, + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertCommonPerClusterMessageTokenType.Link, + url: 'license', + } as AlertCommonPerClusterMessageLinkToken, + ], + }; } diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index c5091c36c3bbeb..1bddede52207b2 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -29,6 +29,7 @@ import { CODE_PATH_BEATS, CODE_PATH_APM, KIBANA_ALERTING_ENABLED, + ALERT_TYPES, } from '../../../common/constants'; import { getApmsForClusters } from '../apm/get_apms_for_clusters'; import { i18n } from '@kbn/i18n'; @@ -102,15 +103,8 @@ export async function getClustersFromRequest( if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { if (KIBANA_ALERTING_ENABLED) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - const callCluster = (...args) => callWithRequest(req, ...args); - cluster.alerts = await fetchStatus( - callCluster, - start, - end, - cluster.cluster_uuid, - req.server - ); + const alertsClient = req.getAlertsClient ? req.getAlertsClient() : null; + cluster.alerts = await fetchStatus(alertsClient, ALERT_TYPES, start, end, req.logger); } else { cluster.alerts = await alertsClusterSearch( req, diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 24d8bcaa4397cf..0fa1a5bf144ac9 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -47,6 +47,7 @@ import { PluginSetupContract as AlertingPluginSetupContract, } from '../../alerting/server'; import { getLicenseExpiration } from './alerts/license_expiration'; +import { getClusterState } from './alerts/cluster_state'; import { InfraPluginSetup } from '../../infra/server'; export interface LegacyAPI { @@ -154,6 +155,17 @@ export class Plugin { config.ui.ccs.enabled ) ); + plugins.alerting.registerType( + getClusterState( + async () => { + const coreStart = (await core.getStartServices())[0]; + return coreStart.uiSettings; + }, + cluster, + this.getLogger, + config.ui.ccs.enabled + ) + ); } // Initialize telemetry @@ -165,12 +177,7 @@ export class Plugin { // Register collector objects for stats to show up in the APIs if (plugins.usageCollection) { - registerCollectors( - plugins.usageCollection, - config, - core.metrics.getOpsMetrics$(), - get(legacyConfig, 'kibana.index') - ); + registerCollectors(plugins.usageCollection, config); } // If collection is enabled, create the bulk uploader @@ -269,18 +276,23 @@ export class Plugin { catalogue: ['monitoring'], privileges: null, reserved: { - privilege: { - app: ['monitoring', 'kibana'], - catalogue: ['monitoring'], - savedObject: { - all: [], - read: [], - }, - ui: [], - }, description: i18n.translate('xpack.monitoring.feature.reserved.description', { defaultMessage: 'To grant users access, you should also assign the monitoring_user role.', }), + privileges: [ + { + id: 'monitoring', + privilege: { + app: ['monitoring', 'kibana'], + catalogue: ['monitoring'], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + ], }, }); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js index 56922bd8e87e21..d5a43d32f600a9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js @@ -8,8 +8,12 @@ import { schema } from '@kbn/config-schema'; import { isFunction } from 'lodash'; import { ALERT_TYPE_LICENSE_EXPIRATION, + ALERT_TYPE_CLUSTER_STATE, MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + ALERT_TYPES, } from '../../../../../common/constants'; +import { handleError } from '../../../../lib/errors'; +import { fetchStatus } from '../../../../lib/alerts/fetch_status'; async function createAlerts(req, alertsClient, { selectedEmailActionId }) { const createdAlerts = []; @@ -17,7 +21,21 @@ async function createAlerts(req, alertsClient, { selectedEmailActionId }) { // Create alerts const ALERT_TYPES = { [ALERT_TYPE_LICENSE_EXPIRATION]: { - schedule: { interval: '10s' }, + schedule: { interval: '1m' }, + actions: [ + { + group: 'default', + id: selectedEmailActionId, + params: { + subject: '{{context.subject}}', + message: `{{context.message}}`, + to: ['{{context.to}}'], + }, + }, + ], + }, + [ALERT_TYPE_CLUSTER_STATE]: { + schedule: { interval: '1m' }, actions: [ { group: 'default', @@ -86,4 +104,37 @@ export function createKibanaAlertsRoute(server) { return { alerts, emailResponse }; }, }); + + server.route({ + method: 'POST', + path: '/api/monitoring/v1/alert_status', + config: { + validate: { + payload: schema.object({ + timeRange: schema.object({ + min: schema.string(), + max: schema.string(), + }), + }), + }, + }, + async handler(req, headers) { + const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; + if (!alertsClient) { + return headers.response().code(404); + } + + const start = req.payload.timeRange.min; + const end = req.payload.timeRange.max; + let alerts; + + try { + alerts = await fetchStatus(alertsClient, ALERT_TYPES, start, end, req.logger); + } catch (err) { + throw handleError(err, req); + } + + return { alerts }; + }, + }); } 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 4c109c557fdb0d..1e6c2c4d289aab 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 @@ -350,7 +350,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u description={ @@ -367,7 +367,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u data-test-subj="remoteClusterFormConnectionModeToggle" label={ @@ -443,11 +443,11 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u className="euiTextColor euiTextColor--subdued" > - Use seed nodes by default, or switch to a single proxy address. + Use seed nodes by default, or switch to proxy mode. @@ -534,11 +534,11 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u onClick={[Function]} > - Use a single proxy address + Use proxy mode @@ -687,23 +687,37 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u + + , + } + } /> } + isInvalid={false} label={ } @@ -711,28 +725,31 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u >
@@ -740,20 +757,19 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u
-
- + - +
- + + + , + } + } > - The number of socket connections to open per remote cluster. + A string sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. + + +
@@ -801,37 +845,23 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u
- - , - } - } + defaultMessage="The number of socket connections to open per remote cluster." + id="xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText" + values={Object {}} /> } - isInvalid={false} label={ } @@ -839,31 +869,28 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u >
@@ -871,19 +898,20 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u
-
- + - +
- + - - , - } - } + defaultMessage="The number of socket connections to open per remote cluster." + id="xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText" + values={Object {}} > - A string sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. - - - + The number of socket connections to open per remote cluster.
@@ -1447,7 +1447,7 @@ Array [
- Use seed nodes by default, or switch to a single proxy address. + Use seed nodes by default, or switch to proxy mode.
- Use a single proxy address + Use proxy mode
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js index c98bd73bf83a0a..a392cc96077845 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js @@ -405,30 +405,6 @@ export class RemoteClusterForm extends Component { /> - - } - helpText={ - - } - fullWidth - > - - this.onFieldsChange({ proxySocketConnections: Number(e.target.value) || null }) - } - fullWidth - /> - + + + } + helpText={ + + } + fullWidth + > + + this.onFieldsChange({ proxySocketConnections: Number(e.target.value) || null }) + } + fullWidth + /> + ); } @@ -498,14 +499,14 @@ export class RemoteClusterForm extends Component { <> } checked={mode === PROXY_MODE} @@ -519,15 +520,38 @@ export class RemoteClusterForm extends Component { <> } - iconType="pin" - size="s" - /> + > + + + + ), + searchString: ( + + + + ), + }} + /> + ) : null} diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap index 75bc1f9eea696d..271d61c2242105 100644 --- a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap @@ -2,525 +2,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` Array [ - -
- - -
- -
- - - -
-
- - - - -
- - -
-
- - - -
- -
- - - -
- - -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
-
- - Report - -
-
-
- - Created at - -
-
-
- - Status - -
-
-
- - Actions - -
-
-
- - Loading reports - -
-
-
-
-
- -
- , { throw error; } } + + // Since the contents of the table have changed, we must reset the pagination + // and re-fetch. Otherwise, the Nth page we are on could be empty of jobs. + this.setState(() => ({ page: 0 }), this.fetchJobs); }; return ( @@ -476,34 +480,32 @@ class ReportListingUi extends Component { onSelectionChange: this.onSelectionChange, }; - const search = { - toolsRight: this.renderDeleteButton(), - }; - return ( - + + + {this.state.selectedJobs.length > 0 ? this.renderDeleteButton() : null} + ); } } diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index f065c9cfd90bac..cf970a561b93f9 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -29,6 +29,7 @@ import { initAppAuthorization } from './app_authorization'; import { initAPIAuthorization } from './api_authorization'; import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; import { validateFeaturePrivileges } from './validate_feature_privileges'; +import { validateReservedPrivileges } from './validate_reserved_privileges'; import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; import { APPLICATION_PREFIX } from '../../common/constants'; import { SecurityLicense } from '../../common/licensing'; @@ -121,7 +122,9 @@ export function setupAuthorization({ }, registerPrivilegesWithCluster: async () => { - 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 3d25fc03f568b6..b023c12d35b79a 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 b25aad30a34238..9a8935f80a174f 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 ac386d287cff19..cd2c7faa263c96 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 00000000000000..26af0dadfb2886 --- /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 00000000000000..0915308fc0f894 --- /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 7809b511adda49..99b4e184c071a6 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 4d92505c4aebbc..fee41fc7e36d32 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 b22cec0af5ea83..4f6ff55dbfbb2d 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 96cbac4b48065a..ea74fc92b95ead 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 fb2616619c644b..65a0cabfeb7166 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 96e642b0f45d8b..44c9e9993bf102 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/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2ae29202ede43d..e63e1c8ad2c916 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2523,12 +2523,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 の管理を行うセンターコンソールです。", @@ -9732,7 +9730,6 @@ "xpack.ml.datavisualizer.selector.dataVisualizerTitle": "データビジュアライザー", "xpack.ml.datavisualizer.selector.experimentalBadgeLabel": "実験的", "xpack.ml.datavisualizer.selector.experimentalBadgeTooltipLabel": "実験的機能。フィードバックをお待ちしています。", - "xpack.ml.datavisualizer.selector.importDataDescription": "ログファイルからデータをインポートします。最大 100 MB のファイルをアップロードできます。", "xpack.ml.datavisualizer.selector.importDataTitle": "データのインポート", "xpack.ml.datavisualizer.selector.selectIndexButtonLabel": "インデックスを選択", "xpack.ml.datavisualizer.selector.selectIndexPatternDescription": "既存の Elasticsearch インデックスのデータを可視化します。", @@ -9983,7 +9980,6 @@ "xpack.ml.fileDatavisualizer.welcomeContent.logFilesWithCommonFormatDescription": "タイムスタンプの一般的フォーマットのログファイル", "xpack.ml.fileDatavisualizer.welcomeContent.newlineDelimitedJsonDescription": "改行区切りの JSON", "xpack.ml.fileDatavisualizer.welcomeContent.supportedFileFormatDescription": "ファイルデータビジュアライザーはこれらのファイル形式をサポートしています:", - "xpack.ml.fileDatavisualizer.welcomeContent.uploadedFilesAllowedSizeDescription": "最大 100 MB のファイルをアップロードできます。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileDescription": "ファイルデータビジュアライザーは、ログファイルのフィールドとメトリックの理解に役立ちます。ファイルをアップロードして、データを分析し、 Elasticsearch インデックスにインポートするか選択できます。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileTitle": "ログファイルのデータを可視化 {experimentalBadge}", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "実際値が通常値と同じ", @@ -15868,8 +15864,6 @@ "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": "サーバーからメールを送信します。", @@ -15964,9 +15958,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}}}を削除 ", @@ -15990,8 +15981,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": "このコネクターを削除", @@ -16041,7 +16032,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": "下の表現のエラーを修正してください。", @@ -16078,7 +16068,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} コネクター", @@ -16130,9 +16119,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 7f5df15dec83ac..cc75ceb988d972 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2524,12 +2524,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 的中心控制台。", @@ -9735,7 +9733,6 @@ "xpack.ml.datavisualizer.selector.dataVisualizerTitle": "数据可视化工具", "xpack.ml.datavisualizer.selector.experimentalBadgeLabel": "实验性", "xpack.ml.datavisualizer.selector.experimentalBadgeTooltipLabel": "实验功能。我们很乐意听取您的反馈意见。", - "xpack.ml.datavisualizer.selector.importDataDescription": "从日志文件导入数据。您可以上传最大 100 MB 的文件。", "xpack.ml.datavisualizer.selector.importDataTitle": "导入数据", "xpack.ml.datavisualizer.selector.selectIndexButtonLabel": "选择索引", "xpack.ml.datavisualizer.selector.selectIndexPatternDescription": "可视化现有 Elasticsearch 索引中的数据。", @@ -9986,7 +9983,6 @@ "xpack.ml.fileDatavisualizer.welcomeContent.logFilesWithCommonFormatDescription": "具有时间戳通用格式的日志文件", "xpack.ml.fileDatavisualizer.welcomeContent.newlineDelimitedJsonDescription": "换行符分隔的 JSON", "xpack.ml.fileDatavisualizer.welcomeContent.supportedFileFormatDescription": "File Data Visualizer 支持以下文件格式:", - "xpack.ml.fileDatavisualizer.welcomeContent.uploadedFilesAllowedSizeDescription": "您可以上传最大 100 MB 的文件。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileDescription": "File Data Visualizer 可帮助您理解日志文件中的字段和指标。上传文件、分析文件数据,然后选择是否将数据导入 Elasticsearch 索引。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileTitle": "可视化来自日志文件的数据 {experimentalBadge}", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "实际上与典型模式相同", @@ -15872,8 +15868,6 @@ "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": "从您的服务器发送电子邮件。", @@ -15968,9 +15962,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}}} ", @@ -15995,8 +15986,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": "删除此连接器", @@ -16046,7 +16037,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": "表达式包含错误。", @@ -16083,7 +16073,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} 连接器", @@ -16135,9 +16124,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/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 85699cfbd750f0..00000000000000 --- 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 f7a80202dff89a..00000000000000 --- 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 a7d479f922ed1b..af9e34071fd095 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/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx index 6ce954f61bcdbc..d83396f2008298 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx @@ -134,7 +134,7 @@ describe('IndexParamsFields renders', () => { ActionParamsProps >; const actionParams = { - documents: ['test'], + documents: [{ test: 123 }], }; const wrapper = mountWithIntl( { .find('[data-test-subj="actionIndexDoc"]') .first() .prop('value') - ).toBe('"test"'); + ).toBe(`{ + "test": 123 +}`); + expect( + wrapper.find('[data-test-subj="indexDocumentAddVariableButton"]').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 d631882e1f581c..56d9f40e40021d 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,6 +14,10 @@ import { EuiSelect, EuiTitle, EuiIconTip, + EuiPopover, + EuiButtonIcon, + EuiContextMenuPanel, + EuiContextMenuItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -31,6 +35,7 @@ import { getIndexOptions, getIndexPatterns, } from '../../../common/index_controls'; +import { useXJsonMode } from '../../lib/use_x_json_mode'; export function getActionType(): ActionTypeModel { return { @@ -271,9 +276,29 @@ const IndexParamsFields: React.FunctionComponent { const { documents } = actionParams; - + const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode( + documents && documents.length > 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}}}`} + + )); function onDocumentsChange(updatedDocuments: string) { try { const documentsJSON = JSON.parse(updatedDocuments); @@ -291,27 +316,55 @@ 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" + > + + + } > 0 ? documents[0] : {}, null, 2)} - onChange={onDocumentsChange} - width="100%" - height="auto" - minLines={6} - maxLines={30} - isReadOnly={false} - setOptions={{ - showLineNumbers: true, - tabSize: 2, - }} - editorProps={{ - $blockScrolling: Infinity, + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel', + { + defaultMessage: 'Code editor', + } + )} + value={xJson} + onChange={(xjson: string) => { + setXJson(xjson); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(xjson)); }} - showGutter={true} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index fd35db43042759..84d8b6e8caeded 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -39,7 +39,7 @@ export interface PagerDutyActionParams { } export interface IndexActionParams { - documents: string[]; + documents: Array>; } export enum ServerLogLevelOptions { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx index fdb60bd2d91466..c4489a316d2c00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx @@ -39,6 +39,7 @@ describe('webhook connector validation', () => { 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', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.scss new file mode 100644 index 00000000000000..c4d12221e3a01e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.scss @@ -0,0 +1,13 @@ +@mixin padBannerWith($size) { + padding-left: $size; + padding-right: $size; +} + +.alertingHealthCheck__body { + @include padBannerWith(2 * $euiSize); +} + +.alertingFlyoutHealthCheck__body { + @include padBannerWith(2 * $euiSize); + margin-top: $euiSize; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx new file mode 100644 index 00000000000000..5156a6146f3a12 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -0,0 +1,131 @@ +/* + * 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 from 'react'; +import { render } from '@testing-library/react'; + +import { HealthCheck } from './health_check'; + +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('health check', () => { + 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 00000000000000..c967cf5de0771c --- /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 00000000000000..fe001ce294ef4f --- /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 00000000000000..0e956ea56faa9c --- /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 00000000000000..df593d587de3fb --- /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 28bc02ec3392f2..00000000000000 --- 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 9874a3a0697d2c..00000000000000 --- 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 7c8d798984bf27..4d0a9980f2231f 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 ee68b7e269c342..e9cf2a270d180d 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 26ad97f05849d4..e82d268accdd82 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/lib/use_x_json_mode.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/use_x_json_mode.ts new file mode 100644 index 00000000000000..b677924724ffe8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/use_x_json_mode.ts @@ -0,0 +1,25 @@ +/* + * 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 { useState } from 'react'; +import { XJsonMode } from '../../../../../../src/plugins/es_ui_shared/console_lang/ace/modes/x_json'; +import { + collapseLiteralStrings, + expandLiteralStrings, +} from '../../../../../../src/plugins/es_ui_shared/console_lang/lib'; +// @ts-ignore +export const xJsonMode = new XJsonMode(); +export const useXJsonMode = (json: Record | null) => { + const [xJson, setXJson] = useState( + json === null ? '' : expandLiteralStrings(JSON.stringify(json, null, 2)) + ); + + return { + xJson, + setXJson, + xJsonMode, + convertToJson: collapseLiteralStrings, + }; +}; 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 89d37c4d00a112..41564146bb84d9 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 3ade4e6368f96e..6b011ac84bc6f1 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 f9aa2cad8bfc60..2c063ea6b4fa64 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 df7e5d8fe9a78d..e469a501089128 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 3d65b8a799b1b1..70ad1cae6c1d14 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 9331fe17046949..4fa1e7e4c6e4df 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 fc07171347e5e5..47e058f4739463 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 e44e20751b315e..0620ced6365a96 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 6fcfb463c4c778..916ba368e07320 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 3f27a7860bafa0..4255eca83be473 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 108cc724aa4074..66aa02e1930a3e 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 afd3299f0c2bbc..5d59180ff572b0 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 fb3ff9ceb09265..99dd7b63383fbf 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 7dfaa7b918f705..7f78d327d01225 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/plugins/watcher/public/plugin.ts b/x-pack/plugins/watcher/public/plugin.ts index 9bee7e60273e48..6de21bc27d48c7 100644 --- a/x-pack/plugins/watcher/public/plugin.ts +++ b/x-pack/plugins/watcher/public/plugin.ts @@ -12,7 +12,6 @@ import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { LicenseStatus } from '../common/types/license_status'; import { ILicense } from '../../licensing/public'; -import { TimeBuckets } from './legacy'; import { PLUGIN } from '../common/constants'; import { Dependencies } from './types'; @@ -41,6 +40,7 @@ export class WatcherUIPlugin implements Plugin { const [core] = await getStartServices(); const { i18n: i18nDep, docLinks, savedObjects } = core; const { boot } = await import('./application/boot'); + const { TimeBuckets } = await import('./legacy'); return boot({ // Skip the first license status, because that's already been used to determine diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 5fb1afa7d584f2..4d32a5ae9f53c4 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 e228f6c1f81c6f..6001dd531cfae8 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 01eaf92da33fe5..612eba858ea0b0 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 cfc04663c6a4f3..eeb0818b5fbabd 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 f4ea568cf08c34..e9d3e6c542442b 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 48f348e1b834d8..054f8f6141817e 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 8afa43bfea21e6..e00589b7e85b7b 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 da83dbf8c47e26..fd996ea4507bab 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 43a3861491467f..922315eba5a5c6 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 6fca330887c3e6..011e47cf11b394 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 bed4c805aaf573..c84b089d48c85e 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 89c5b4f451f82b..80b512f3fb5e3f 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 c6960a4eedd256..d7ec2e78ccb308 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 a792efede07ee0..6cafbeb8c6ea8b 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 3713e9c24419f7..874d42ac04736a 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 efd707b59cd343..c70c289194abbd 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 283e51352c2726..26a811d2cc512e 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 acbc9edc1f2fba..00000000000000 --- 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 0f896bfaa0af9b..a4a13441fb7668 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 00000000000000..517c64f178af5b --- /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 fb2be8c86f4e8b..75544b7fd41697 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 18a0ecc23c1e14..2593f342a8a862 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 fb0c5e13c0720d..05d26aaaed2ec3 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/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index ad495d45054041..887be6b85b100c 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -139,7 +139,7 @@ export default function({ getService }: FtrProviderContext) { .expect(200); expect(body.total).to.eql(2); const resultIps: string[] = [].concat( - ...body.hosts.map((metadata: Record) => metadata.host.ip) + ...body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.ip) ); expect(resultIps).to.eql([ '10.192.213.130', @@ -164,7 +164,7 @@ export default function({ getService }: FtrProviderContext) { .expect(200); expect(body.total).to.eql(2); const resultOsVariantValue: Set = new Set( - body.hosts.map((metadata: Record) => metadata.host.os.variant) + body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.os.variant) ); expect(Array.from(resultOsVariantValue)).to.eql([variantValue]); expect(body.hosts.length).to.eql(2); @@ -182,17 +182,17 @@ export default function({ getService }: FtrProviderContext) { }) .expect(200); expect(body.total).to.eql(1); - const resultIp: string = body.hosts[0].host.ip.filter( + const resultIp: string = body.hosts[0].metadata.host.ip.filter( (ip: string) => ip === targetEndpointIp ); expect(resultIp).to.eql([targetEndpointIp]); - expect(body.hosts[0].event.created).to.eql(1579881969541); + expect(body.hosts[0].metadata.event.created).to.eql(1579881969541); expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); - it('metadata api should return the endpoint based on the elastic agent id', async () => { + it('metadata api should return the endpoint based on the elastic agent id, and status should be error', async () => { const targetEndpointId = 'fc0ff548-feba-41b6-8367-65e8790d0eaf'; const targetElasticAgentId = '023fa40c-411d-4188-a941-4147bfadd095'; const { body } = await supertest @@ -203,11 +203,12 @@ export default function({ getService }: FtrProviderContext) { }) .expect(200); expect(body.total).to.eql(1); - const resultHostId: string = body.hosts[0].host.id; - const resultElasticAgentId: string = body.hosts[0].elastic.agent.id; + const resultHostId: string = body.hosts[0].metadata.host.id; + const resultElasticAgentId: string = body.hosts[0].metadata.elastic.agent.id; expect(resultHostId).to.eql(targetEndpointId); expect(resultElasticAgentId).to.eql(targetElasticAgentId); - expect(body.hosts[0].event.created).to.eql(1579881969541); + expect(body.hosts[0].metadata.event.created).to.eql(1579881969541); + expect(body.hosts[0].host_status).to.eql('error'); expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index bcba156a64f0e4..4e21faa610bfe2 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 77293ddff3f9f6..9bec3fd076e865 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 0b29fc1cac7de7..1f9eac148b3029 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 143899a61ffb96..47c699e3094916 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 00000000000000..882fb6fea36861 --- /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/yarn.lock b/yarn.lock index 77ab69c715573c..3f04b2d26a0139 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"